blob: 690efbc1cffaf4a7f6ff7867189bb947d66b4905 [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/base/platform/semaphore.h"
#include "src/heap/heap.h"
#include "src/heap/parked-scope-inl.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);
v8::Isolate::Scope isolate_scope(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);
}
InvokeMajorGC(i_client_isolate);
v8::platform::PumpMessageLoop(i::V8::GetCurrentPlatform(),
client_isolate);
});
}
};
} // namespace
TEST_F(SharedHeapTest, ConcurrentAllocationInSharedOldSpace) {
i_isolate()->main_thread_local_isolate()->BlockMainThreadWhileParked(
[](const ParkedScope& parked) {
std::vector<std::unique_ptr<SharedOldSpaceAllocationThread>> threads;
const int kThreads = 4;
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());
}
InvokeMajorGC(i_client_isolate);
v8::platform::PumpMessageLoop(i::V8::GetCurrentPlatform(),
client_isolate);
});
}
};
} // namespace
TEST_F(SharedHeapTest, ConcurrentAllocationInSharedLargeOldSpace) {
i_isolate()->main_thread_local_isolate()->BlockMainThreadWhileParked(
[](const ParkedScope& parked) {
std::vector<std::unique_ptr<SharedLargeOldSpaceAllocationThread>>
threads;
const int kThreads = 4;
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()->NewContextlessMap(
NATIVE_CONTEXT_TYPE, kVariableSizeSentinel,
TERMINAL_FAST_ELEMENTS_KIND, 0, AllocationType::kSharedMap);
}
InvokeMajorGC(i_client_isolate);
v8::platform::PumpMessageLoop(i::V8::GetCurrentPlatform(),
client_isolate);
});
}
};
} // namespace
TEST_F(SharedHeapTest, ConcurrentAllocationInSharedMapSpace) {
i_isolate()->main_thread_local_isolate()->BlockMainThreadWhileParked(
[](const ParkedScope& parked) {
std::vector<std::unique_ptr<SharedMapSpaceAllocationThread>> threads;
const int kThreads = 4;
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::InvokeMajorGC(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++) {
Tagged<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;
i_isolate()->main_thread_local_isolate()->BlockMainThreadWhileParked(
[]() { 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;
i_isolate()->main_thread_local_isolate()->BlockMainThreadWhileParked(
[](const ParkedScope& parked) {
std::vector<std::unique_ptr<SharedFixedArrayAllocationThread>> threads;
const int kThreads = 4;
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(
bool wait_while_parked, v8::base::Semaphore* sema_ready = nullptr,
v8::base::Semaphore* sema_execute_start = nullptr,
v8::base::Semaphore* sema_execute_complete = nullptr)
: ParkingThread(Options("ConcurrentThread")),
sema_ready_(sema_ready),
sema_execute_start_(sema_execute_start),
sema_execute_complete_(sema_execute_complete),
wait_while_parked_(wait_while_parked) {}
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_) {
if (wait_while_parked_) {
// Park and wait.
i_client_isolate_->main_thread_local_isolate()
->BlockMainThreadWhileParked(
[this]() { sema_execute_start_->Wait(); });
} else {
// Do not park, but enter a safepoint every now and then.
const auto timeout = base::TimeDelta::FromMilliseconds(100);
do {
i_client_isolate_->main_thread_local_isolate()->heap()->Safepoint();
} while (!sema_execute_start_->WaitFor(timeout));
}
}
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;
v8::base::Semaphore* sema_ready_ = nullptr;
v8::base::Semaphore* sema_execute_start_ = nullptr;
v8::base::Semaphore* sema_execute_complete_ = nullptr;
Callback* setup_callback_ = nullptr;
Callback* execute_callback_ = nullptr;
Callback* complete_callback_ = nullptr;
bool wait_while_parked_;
};
template <typename State, typename ThreadState, bool wait_while_parked>
class SharedHeapTestBase : public TestJSSharedMemoryWithNativeContext {
public:
using TestType = SharedHeapTestBase<State, ThreadState, wait_while_parked>;
using Callback = void(TestType*);
using ThreadType = ConcurrentThread<ThreadState>;
using ThreadCallback = typename ThreadType::Callback;
SharedHeapTestBase()
: thread_(std::make_unique<ThreadType>(wait_while_parked, &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_.Wait();
if (execute_callback_) execute_callback_(this);
sema_execute_start_.Signal();
sema_execute_complete_.Wait();
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_;
v8::base::Semaphore sema_ready_{0};
v8::base::Semaphore sema_execute_start_{0};
v8::base::Semaphore sema_execute_complete_{0};
Callback* setup_callback_ = nullptr;
Callback* execute_callback_ = nullptr;
Callback* complete_callback_ = nullptr;
Callback* teardown_callback_ = nullptr;
};
} // namespace
#define TEST_SCENARIO(test_class, test_method, test_name, allocation, space) \
TEST_F(test_class, test_name) { \
test_method<test_class, allocation, space>(this); \
}
#define TEST_ALL_SCENARIA(test_class, test_prefix, test_method) \
TEST_SCENARIO(test_class, test_method, test_prefix##YoungYoung, \
AllocationType::kYoung, NEW_SPACE) \
TEST_SCENARIO(test_class, test_method, test_prefix##YoungOld, \
AllocationType::kYoung, OLD_SPACE) \
TEST_SCENARIO(test_class, test_method, test_prefix##OldYoung, \
AllocationType::kOld, NEW_SPACE) \
TEST_SCENARIO(test_class, test_method, test_prefix##OldOld, \
AllocationType::kOld, OLD_SPACE) \
TEST_SCENARIO(test_class, test_method, test_prefix##SharedYoung, \
AllocationType::kSharedOld, NEW_SPACE) \
TEST_SCENARIO(test_class, test_method, test_prefix##SharedOld, \
AllocationType::kSharedOld, OLD_SPACE)
namespace {
// Testing the shared heap using ordinary (indirect) handles.
struct StateWithHandle {
base::Optional<HandleScope> scope;
Handle<FixedArray> handle;
Global<v8::FixedArray> weak;
};
template <AllocationType allocation, AllocationSpace space, int size>
void AllocateWithHandle(Isolate* isolate, StateWithHandle* 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(size, allocation);
Local<v8::FixedArray> l = Utils::FixedArrayToLocal(state->handle);
state->weak.Reset(reinterpret_cast<v8::Isolate*>(isolate), l);
state->weak.SetWeak();
}
using SharedHeapTestStateWithHandleParked =
SharedHeapTestBase<StateWithHandle, StateWithHandle, true>;
using SharedHeapTestStateWithHandleUnparked =
SharedHeapTestBase<StateWithHandle, StateWithHandle, false>;
void InvokeGC(AllocationSpace space, Isolate* isolate) {
space == NEW_SPACE ? InvokeMinorGC(isolate) : InvokeMajorGC(isolate);
}
template <typename TestType, AllocationType allocation, AllocationSpace space>
void ToEachTheirOwnWithHandle(TestType* test) {
using ThreadType = typename TestType::ThreadType;
ThreadType* thread = test->thread();
// Install all the callbacks.
test->with_setup([](TestType* test) {
AllocateWithHandle<allocation, space, 10>(test->i_isolate(), test->state());
});
thread->with_setup([](ThreadType* thread) {
AllocateWithHandle<allocation, space, 20>(thread->i_client_isolate(),
thread->state());
});
test->with_execute(
[](TestType* test) { InvokeGC(space, test->i_isolate()); });
thread->with_execute(
[](ThreadType* thread) { InvokeGC(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.
InvokeGC(space, thread->i_client_isolate());
});
test->with_teardown([](TestType* test) {
test->state()->scope.reset(); // Deallocate the handle scope.
InvokeGC(space, test->i_isolate());
});
// Perform the test.
test->Interact();
}
} // namespace
TEST_ALL_SCENARIA(SharedHeapTestStateWithHandleParked, ToEachTheirOwn,
ToEachTheirOwnWithHandle)
TEST_ALL_SCENARIA(SharedHeapTestStateWithHandleUnparked, ToEachTheirOwn,
ToEachTheirOwnWithHandle)
#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;
};
template <AllocationType allocation, AllocationSpace space, int size>
void AllocateWithRawPointer(Isolate* isolate, StateWithRawPointer* state) {
// Allocate a fixed array, keep a raw pointer and a weak reference.
HandleScope scope(isolate);
Handle<FixedArray> h = isolate->factory()->NewFixedArray(size, allocation);
state->ptr = (*h).ptr();
Local<v8::FixedArray> l = Utils::FixedArrayToLocal(h, isolate);
state->weak.Reset(reinterpret_cast<v8::Isolate*>(isolate), l);
state->weak.SetWeak();
}
using SharedHeapTestStateWithRawPointerParked =
SharedHeapTestBase<StateWithRawPointer, StateWithRawPointer, true>;
using SharedHeapTestStateWithRawPointerUnparked =
SharedHeapTestBase<StateWithRawPointer, StateWithRawPointer, false>;
template <typename TestType, AllocationType allocation, AllocationSpace space>
void ToEachTheirOwnWithRawPointer(TestType* test) {
using ThreadType = typename TestType::ThreadType;
ThreadType* thread = test->thread();
// Install all the callbacks.
test->with_setup([](TestType* test) {
AllocateWithRawPointer<allocation, space, 10>(test->i_isolate(),
test->state());
});
thread->with_setup([](ThreadType* thread) {
AllocateWithRawPointer<allocation, space, 20>(thread->i_client_isolate(),
thread->state());
});
test->with_execute(
[](TestType* test) { InvokeGC(space, test->i_isolate()); });
thread->with_execute(
[](ThreadType* thread) { InvokeGC(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());
InvokeGC(space, thread->i_client_isolate());
});
test->with_teardown(
[](TestType* test) { InvokeGC(space, test->i_isolate()); });
// Perform the test.
test->Interact();
}
} // namespace
TEST_ALL_SCENARIA(SharedHeapTestStateWithRawPointerParked, ToEachTheirOwn,
ToEachTheirOwnWithRawPointer)
TEST_ALL_SCENARIA(SharedHeapTestStateWithRawPointerUnparked, ToEachTheirOwn,
ToEachTheirOwnWithRawPointer)
#endif // V8_ENABLE_CONSERVATIVE_STACK_SCANNING
#undef TEST_SCENARIO
#undef TEST_ALL_SCENARIA
} // namespace internal
} // namespace v8
#endif // V8_CAN_CREATE_SHARED_HEAP_BOOL