blob: 4110cd0a5a6d960c0148861b4fc75eca0df9ac71 [file] [log] [blame]
// Copyright 2022 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/handles/handles.h"
#include "src/handles/traced-handles.h"
#include "test/unittests/heap/cppgc-js/unified-heap-utils.h"
#include "test/unittests/heap/heap-utils.h"
namespace v8::internal {
namespace {
constexpr uint16_t kClassIdToOptimize = 23;
using EmbedderRootsHandlerTest = TestWithHeapInternalsAndContext;
class V8_NODISCARD TemporaryEmbedderRootsHandleScope final {
public:
TemporaryEmbedderRootsHandleScope(v8::Isolate* isolate,
v8::EmbedderRootsHandler* handler)
: isolate_(isolate) {
isolate_->SetEmbedderRootsHandler(handler);
}
~TemporaryEmbedderRootsHandleScope() {
isolate_->SetEmbedderRootsHandler(nullptr);
}
private:
v8::Isolate* const isolate_;
};
// EmbedderRootsHandler that can optimize Scavenger handling when used with
// TracedReference.
class ClearingEmbedderRootsHandler final : public v8::EmbedderRootsHandler {
public:
explicit ClearingEmbedderRootsHandler(uint16_t class_id_to_optimize)
: class_id_to_optimize_(class_id_to_optimize) {}
bool IsRoot(const v8::TracedReference<v8::Value>& handle) final {
return handle.WrapperClassId() != class_id_to_optimize_;
}
void ResetRoot(const v8::TracedReference<v8::Value>& handle) final {
if (handle.WrapperClassId() != class_id_to_optimize_) return;
// Convention (for test): Objects that are optimized have their first field
// set as a back pointer.
BasicTracedReference<v8::Value>* original_handle =
reinterpret_cast<BasicTracedReference<v8::Value>*>(
v8::Object::GetAlignedPointerFromInternalField(
handle.As<v8::Object>(), 0));
original_handle->Reset();
}
private:
const uint16_t class_id_to_optimize_;
};
// EmbedderRootsHandler that forwards handle processing to the main thread and
// makes sure that concurrently processed handles get reprocessed if needed.
class MainThreadForwardingRootHandler final : public v8::EmbedderRootsHandler {
public:
bool IsRoot(const v8::TracedReference<v8::Value>& handle) { return false; }
void ResetRoot(const v8::TracedReference<v8::Value>& handle) final {
{
auto* address = *reinterpret_cast<const Address* const*>(&handle);
base::LockGuard<base::Mutex> _(&mutex);
if (auto it = unprocessed_.find(address); it != unprocessed_.end())
unprocessed_.erase(it);
}
// Convention (for test): Set first field as a back pointer.
BasicTracedReference<v8::Value>* original_handle =
reinterpret_cast<BasicTracedReference<v8::Value>*>(
v8::Object::GetAlignedPointerFromInternalField(
handle.As<v8::Object>(), 0));
original_handle->Reset();
}
bool TryResetRoot(const v8::TracedReference<v8::Value>& handle) final {
base::LockGuard<base::Mutex> _(&mutex);
unprocessed_.insert(*reinterpret_cast<const Address* const*>(&handle));
return false;
}
const auto& unprocessed() const { return unprocessed_; }
private:
base::Mutex mutex;
std::unordered_set<const Address*> unprocessed_;
};
template <typename T>
void SetupOptimizedAndNonOptimizedHandle(v8::Isolate* isolate,
uint16_t optimized_class_id,
T* optimized_handle,
T* non_optimized_handle) {
v8::HandleScope scope(isolate);
v8::Local<v8::Object> optimized_object = WrapperHelper::CreateWrapper(
isolate->GetCurrentContext(), optimized_handle, nullptr);
EXPECT_TRUE(optimized_handle->IsEmpty());
*optimized_handle = T(isolate, optimized_object);
EXPECT_FALSE(optimized_handle->IsEmpty());
optimized_handle->SetWrapperClassId(optimized_class_id);
v8::Local<v8::Object> non_optimized_object = WrapperHelper::CreateWrapper(
isolate->GetCurrentContext(), nullptr, nullptr);
EXPECT_TRUE(non_optimized_handle->IsEmpty());
*non_optimized_handle = T(isolate, non_optimized_object);
EXPECT_FALSE(non_optimized_handle->IsEmpty());
}
} // namespace
TEST_F(EmbedderRootsHandlerTest,
TracedReferenceNoDestructorReclaimedOnScavenge) {
if (v8_flags.single_generation) return;
ManualGCScope manual_gc(i_isolate());
v8::HandleScope scope(v8_isolate());
DisableConservativeStackScanningScopeForTesting no_stack_scanning(heap());
ClearingEmbedderRootsHandler handler(kClassIdToOptimize);
TemporaryEmbedderRootsHandleScope roots_handler_scope(v8_isolate(), &handler);
auto* traced_handles = i_isolate()->traced_handles();
const size_t initial_count = traced_handles->used_node_count();
auto* optimized_handle = new v8::TracedReference<v8::Value>();
auto* non_optimized_handle = new v8::TracedReference<v8::Value>();
SetupOptimizedAndNonOptimizedHandle(v8_isolate(), kClassIdToOptimize,
optimized_handle, non_optimized_handle);
EXPECT_EQ(initial_count + 2, traced_handles->used_node_count());
CollectGarbage(NEW_SPACE);
EXPECT_EQ(initial_count + 1, traced_handles->used_node_count());
EXPECT_TRUE(optimized_handle->IsEmpty());
delete optimized_handle;
EXPECT_FALSE(non_optimized_handle->IsEmpty());
non_optimized_handle->Reset();
delete non_optimized_handle;
EXPECT_EQ(initial_count, traced_handles->used_node_count());
}
namespace {
void ConstructJSObject(v8::Isolate* isolate, v8::Local<v8::Context> context,
v8::TracedReference<v8::Object>* handle) {
v8::HandleScope scope(isolate);
v8::Local<v8::Object> object(v8::Object::New(isolate));
EXPECT_FALSE(object.IsEmpty());
*handle = v8::TracedReference<v8::Object>(isolate, object);
EXPECT_FALSE(handle->IsEmpty());
}
template <typename T>
void ConstructJSApiObject(v8::Isolate* isolate, v8::Local<v8::Context> context,
T* global) {
v8::HandleScope scope(isolate);
v8::Local<v8::Object> object =
WrapperHelper::CreateWrapper(context, nullptr, nullptr);
EXPECT_FALSE(object.IsEmpty());
*global = T(isolate, object);
EXPECT_FALSE(global->IsEmpty());
}
enum class SurvivalMode { kSurvives, kDies };
template <typename ModifierFunction, typename ConstructTracedReferenceFunction,
typename GCFunction>
void TracedReferenceTest(v8::Isolate* isolate,
ConstructTracedReferenceFunction construct_function,
ModifierFunction modifier_function,
GCFunction gc_function, SurvivalMode survives) {
auto i_isolate = reinterpret_cast<i::Isolate*>(isolate);
DisableConservativeStackScanningScopeForTesting no_stack_scanning(
i_isolate->heap());
v8::HandleScope scope(isolate);
auto* traced_handles = i_isolate->traced_handles();
const size_t initial_count = traced_handles->used_node_count();
auto gc_invisible_handle =
std::make_unique<v8::TracedReference<v8::Object>>();
construct_function(isolate, isolate->GetCurrentContext(),
gc_invisible_handle.get());
ASSERT_TRUE(IsNewObjectInCorrectGeneration(isolate, *gc_invisible_handle));
modifier_function(*gc_invisible_handle);
const size_t after_modification_count = traced_handles->used_node_count();
gc_function();
// Cannot check the handle as it is not explicitly cleared by the GC. Instead
// check the handles count.
CHECK_IMPLIES(survives == SurvivalMode::kSurvives,
after_modification_count == traced_handles->used_node_count());
CHECK_IMPLIES(survives == SurvivalMode::kDies,
initial_count == traced_handles->used_node_count());
}
} // namespace
TEST_F(EmbedderRootsHandlerTest, TracedReferenceWrapperClassId) {
ManualGCScope manual_gc(i_isolate());
v8::HandleScope scope(v8_isolate());
v8::TracedReference<v8::Object> traced;
ConstructJSObject(v8_isolate(), v8_isolate()->GetCurrentContext(), &traced);
EXPECT_EQ(0, traced.WrapperClassId());
traced.SetWrapperClassId(17);
EXPECT_EQ(17, traced.WrapperClassId());
}
// EmbedderRootsHandler does not affect full GCs.
TEST_F(EmbedderRootsHandlerTest,
TracedReferenceToUnmodifiedJSObjectDiesOnFullGC) {
// When stressing incremental marking, a write barrier may keep the object
// alive.
if (v8_flags.stress_incremental_marking) return;
ClearingEmbedderRootsHandler handler(kClassIdToOptimize);
TemporaryEmbedderRootsHandleScope roots_handler_scope(v8_isolate(), &handler);
TracedReferenceTest(
v8_isolate(), ConstructJSObject,
[](const TracedReference<v8::Object>&) {},
[this]() { CollectGarbage(OLD_SPACE); }, SurvivalMode::kDies);
}
// EmbedderRootsHandler does not affect full GCs.
TEST_F(
EmbedderRootsHandlerTest,
TracedReferenceToUnmodifiedJSObjectDiesOnFullGCEvenWhenPointeeIsHeldAlive) {
ManualGCScope manual_gcs(i_isolate());
ClearingEmbedderRootsHandler handler(kClassIdToOptimize);
TemporaryEmbedderRootsHandleScope roots_handler_scope(v8_isolate(), &handler);
// The TracedReference itself will die as it's not found by the full GC. The
// pointee will be kept alive through other means.
v8::Global<v8::Object> strong_global;
TracedReferenceTest(
v8_isolate(), ConstructJSObject,
[this, &strong_global](const TracedReference<v8::Object>& handle) {
v8::HandleScope scope(v8_isolate());
strong_global =
v8::Global<v8::Object>(v8_isolate(), handle.Get(v8_isolate()));
},
[this, &strong_global]() {
CollectGarbage(OLD_SPACE);
strong_global.Reset();
},
SurvivalMode::kDies);
}
// EmbedderRootsHandler does not affect non-API objects.
TEST_F(EmbedderRootsHandlerTest,
TracedReferenceToUnmodifiedJSObjectSurvivesYoungGC) {
if (v8_flags.single_generation) return;
ManualGCScope manual_gc(i_isolate());
ClearingEmbedderRootsHandler handler(kClassIdToOptimize);
TemporaryEmbedderRootsHandleScope roots_handler_scope(v8_isolate(), &handler);
TracedReferenceTest(
v8_isolate(), ConstructJSObject,
[](const TracedReference<v8::Object>&) {},
[this]() { CollectGarbage(NEW_SPACE); }, SurvivalMode::kSurvives);
}
// EmbedderRootsHandler does not affect non-API objects, even when the handle
// has a wrapper class id that allows for reclamation.
TEST_F(
EmbedderRootsHandlerTest,
TracedReferenceToUnmodifiedJSObjectSurvivesYoungGCWhenExcludedFromRoots) {
if (v8_flags.single_generation) return;
ManualGCScope manual_gc(i_isolate());
ClearingEmbedderRootsHandler handler(kClassIdToOptimize);
TemporaryEmbedderRootsHandleScope roots_handler_scope(v8_isolate(), &handler);
TracedReferenceTest(
v8_isolate(), ConstructJSObject,
[](TracedReference<v8::Object>& handle) {
handle.SetWrapperClassId(kClassIdToOptimize);
},
[this]() { CollectGarbage(NEW_SPACE); }, SurvivalMode::kSurvives);
}
// EmbedderRootsHandler does not affect API objects for handles that have
// their class ids not set up.
TEST_F(EmbedderRootsHandlerTest,
TracedReferenceToUnmodifiedJSApiObjectSurvivesScavengePerDefault) {
if (v8_flags.single_generation) return;
ManualGCScope manual_gc(i_isolate());
ClearingEmbedderRootsHandler handler(kClassIdToOptimize);
TemporaryEmbedderRootsHandleScope roots_handler_scope(v8_isolate(), &handler);
TracedReferenceTest(
v8_isolate(), ConstructJSApiObject<TracedReference<v8::Object>>,
[](const TracedReference<v8::Object>&) {},
[this]() { CollectGarbage(NEW_SPACE); }, SurvivalMode::kSurvives);
}
// EmbedderRootsHandler resets API objects for handles that have their class ids
// set to being optimized.
TEST_F(
EmbedderRootsHandlerTest,
TracedReferenceToUnmodifiedJSApiObjectDiesOnScavengeWhenExcludedFromRoots) {
if (v8_flags.single_generation) return;
ManualGCScope manual_gc(i_isolate());
ClearingEmbedderRootsHandler handler(kClassIdToOptimize);
TemporaryEmbedderRootsHandleScope roots_handler_scope(v8_isolate(), &handler);
TracedReferenceTest(
v8_isolate(), ConstructJSApiObject<TracedReference<v8::Object>>,
[this](TracedReference<v8::Object>& handle) {
handle.SetWrapperClassId(kClassIdToOptimize);
{
HandleScope handles(i_isolate());
auto local = handle.Get(v8_isolate());
local->SetAlignedPointerInInternalField(0, &handle);
}
},
[this]() { CollectGarbage(NEW_SPACE); }, SurvivalMode::kDies);
}
// Test that concurrently processed handles are reprocessed on the main thread.
TEST_F(EmbedderRootsHandlerTest, ReprocessConcurrentHandlesOnTheMainThread) {
static constexpr size_t kHandlesToConstruct = 1 << 14;
if (v8_flags.single_generation) return;
ManualGCScope manual_gc(i_isolate());
MainThreadForwardingRootHandler handler;
TemporaryEmbedderRootsHandleScope roots_handler_scope(v8_isolate(), &handler);
auto i_isolate = reinterpret_cast<i::Isolate*>(v8_isolate());
DisableConservativeStackScanningScopeForTesting no_stack_scanning(
i_isolate->heap());
std::vector<std::unique_ptr<v8::TracedReference<v8::Object>>> traced_refs;
{
v8::HandleScope scope(v8_isolate());
for (size_t i = 0; i < kHandlesToConstruct; ++i) {
auto gc_invisible_handle =
std::make_unique<v8::TracedReference<v8::Object>>();
v8::Local<v8::Object> object = WrapperHelper::CreateWrapper(
v8_isolate()->GetCurrentContext(), nullptr, nullptr);
EXPECT_FALSE(object.IsEmpty());
*gc_invisible_handle =
v8::TracedReference<v8::Object>(v8_isolate(), object);
object->SetAlignedPointerInInternalField(0, gc_invisible_handle.get());
EXPECT_FALSE(gc_invisible_handle->IsEmpty());
ASSERT_TRUE(
IsNewObjectInCorrectGeneration(v8_isolate(), *gc_invisible_handle));
traced_refs.push_back(std::move(gc_invisible_handle));
}
}
CollectGarbage(NEW_SPACE);
EXPECT_TRUE(handler.unprocessed().empty());
}
} // namespace v8::internal