api,heap: Avoid reaching out to embedder memory on TracedGlobal reset
Avoid clearing the memory on the embedder-side of a TracedGlobal handle. When using destructors in TracedGlobal this is safe as long as the embedder reports the handle on tracing GCs. If the embedder does not report a handle it is assumed that the containing object is dead as well. Without using destructors the same argument holds for tracing GCs. In addition, embedders using the optimization of clearing references on non-tracing GCs are expected to clear the reference in ResetHandleInNonTracingGC. It is suggested that only expert embedders make use of (a) no destructors and (b) IsRootForNonTracingGC. Change-Id: Ia417c0eb0860094fcaa554e7046d38abac905714 Bug: chromium:995684 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/1763539 Commit-Queue: Michael Lippautz <mlippautz@chromium.org> Reviewed-by: Ulan Degenbaev <ulan@chromium.org> Cr-Commit-Position: refs/heads/master@{#63362}
This commit is contained in:
parent
116cbea5ed
commit
0af80a3796
42
include/v8.h
42
include/v8.h
@ -798,10 +798,13 @@ template <typename T>
|
||||
struct TracedGlobalTrait {
|
||||
/**
|
||||
* Specifies whether |TracedGlobal<T>| should clear its handle on destruction.
|
||||
* The handle is cleared upon garbage collection as well when the object that
|
||||
* it is referring to is considered as unreachable. It is the responsibility
|
||||
* of the embedder to ensure that the memory holding |TracedGlobal<T>| is
|
||||
* still alive at that point in time.
|
||||
*
|
||||
* V8 will *not* clear the embedder-side memory of the handle. The embedder is
|
||||
* expected to report all |TracedGlobal<T>| handles through
|
||||
* |EmbedderHeapTracer| upon garabge collection.
|
||||
*
|
||||
* See |EmbedderHeapTracer::IsRootForNonTracingGC| for handling with
|
||||
* non-tracing GCs in V8.
|
||||
*/
|
||||
static constexpr bool kRequiresExplicitDestruction = true;
|
||||
};
|
||||
@ -7426,14 +7429,37 @@ class V8_EXPORT EmbedderHeapTracer {
|
||||
/**
|
||||
* Returns true if the TracedGlobal handle should be considered as root for
|
||||
* the currently running non-tracing garbage collection and false otherwise.
|
||||
* The default implementation will keep all TracedGlobal references as roots.
|
||||
*
|
||||
* Default implementation will keep all TracedGlobal references as roots.
|
||||
* If this returns false, then V8 may decide that the object referred to by
|
||||
* such a handle is reclaimed. In that case:
|
||||
* - No action is required if handles are used with destructors.
|
||||
* - When run without destructors (by specializing
|
||||
* |TracedGlobalTrait::kRequiresExplicitDestruction|) V8 calls
|
||||
* |ResetHandleInNonTracingGC|.
|
||||
*
|
||||
* Note that the |handle| is different from the |TracedGlobal<T>| handle that
|
||||
* the embedder holds for retaining the object. The embedder may use
|
||||
* |TracedGlobal<T>::WrapperClassId()| to distinguish cases where it wants
|
||||
* handles to be treated as roots from not being treated as roots.
|
||||
*/
|
||||
virtual bool IsRootForNonTracingGC(
|
||||
const v8::TracedGlobal<v8::Value>& handle) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in combination with |IsRootForNonTracingGC|. Called by V8 when an
|
||||
* object that is backed by a handle is reclaimed by a non-tracing garbage
|
||||
* collection. It is up to the embedder to reset the original handle.
|
||||
*
|
||||
* Note that the |handle| is different from the |TracedGlobal<T>| handle that
|
||||
* the embedder holds for retaining the object. It is up to the embedder to
|
||||
* find the orignal |TracedGlobal<T>| handle via the object or class id.
|
||||
*/
|
||||
virtual void ResetHandleInNonTracingGC(
|
||||
const v8::TracedGlobal<v8::Value>& handle) {}
|
||||
|
||||
/*
|
||||
* Called by the embedder to immediately perform a full garbage collection.
|
||||
*
|
||||
@ -8996,7 +9022,8 @@ class V8_EXPORT V8 {
|
||||
internal::Address* handle);
|
||||
static internal::Address* GlobalizeTracedReference(internal::Isolate* isolate,
|
||||
internal::Address* handle,
|
||||
internal::Address* slot);
|
||||
internal::Address* slot,
|
||||
bool has_destructor);
|
||||
static void MoveGlobalReference(internal::Address** from,
|
||||
internal::Address** to);
|
||||
static void MoveTracedGlobalReference(internal::Address** from,
|
||||
@ -10095,7 +10122,8 @@ T* TracedGlobal<T>::New(Isolate* isolate, T* that, void* slot) {
|
||||
internal::Address* p = reinterpret_cast<internal::Address*>(that);
|
||||
return reinterpret_cast<T*>(V8::GlobalizeTracedReference(
|
||||
reinterpret_cast<internal::Isolate*>(isolate), p,
|
||||
reinterpret_cast<internal::Address*>(slot)));
|
||||
reinterpret_cast<internal::Address*>(slot),
|
||||
TracedGlobalTrait<TracedGlobal<T>>::kRequiresExplicitDestruction));
|
||||
}
|
||||
|
||||
template <class T>
|
||||
|
@ -1033,10 +1033,11 @@ i::Address* V8::GlobalizeReference(i::Isolate* isolate, i::Address* obj) {
|
||||
}
|
||||
|
||||
i::Address* V8::GlobalizeTracedReference(i::Isolate* isolate, i::Address* obj,
|
||||
internal::Address* slot) {
|
||||
internal::Address* slot,
|
||||
bool has_destructor) {
|
||||
LOG_API(isolate, TracedGlobal, New);
|
||||
i::Handle<i::Object> result =
|
||||
isolate->global_handles()->CreateTraced(*obj, slot);
|
||||
isolate->global_handles()->CreateTraced(*obj, slot, has_destructor);
|
||||
#ifdef VERIFY_HEAP
|
||||
if (i::FLAG_verify_heap) {
|
||||
i::Object(*obj).ObjectVerify(isolate);
|
||||
|
@ -22,6 +22,10 @@ namespace internal {
|
||||
|
||||
namespace {
|
||||
|
||||
// Specifies whether V8 expects the holder memory of a global handle to be live
|
||||
// or dead.
|
||||
enum class HandleHolder { kLive, kDead };
|
||||
|
||||
constexpr size_t kBlockSize = 256;
|
||||
|
||||
} // namespace
|
||||
@ -532,7 +536,8 @@ class GlobalHandles::Node final : public NodeBase<GlobalHandles::Node> {
|
||||
set_state(NEAR_DEATH);
|
||||
}
|
||||
|
||||
void ResetPhantomHandle() {
|
||||
void ResetPhantomHandle(HandleHolder handle_holder) {
|
||||
DCHECK_EQ(HandleHolder::kLive, handle_holder);
|
||||
DCHECK_EQ(PHANTOM_WEAK_RESET_HANDLE, weakness_type());
|
||||
DCHECK_EQ(PENDING, state());
|
||||
DCHECK_NULL(weak_callback_);
|
||||
@ -614,6 +619,9 @@ class GlobalHandles::TracedNode final
|
||||
bool is_root() const { return IsRoot::decode(flags_); }
|
||||
void set_root(bool v) { flags_ = IsRoot::update(flags_, v); }
|
||||
|
||||
bool has_destructor() const { return HasDestructor::decode(flags_); }
|
||||
void set_has_destructor(bool v) { flags_ = HasDestructor::update(flags_, v); }
|
||||
|
||||
void SetFinalizationCallback(void* parameter,
|
||||
WeakCallbackInfo<void>::Callback callback) {
|
||||
set_parameter(parameter);
|
||||
@ -640,10 +648,12 @@ class GlobalHandles::TracedNode final
|
||||
set_state(NEAR_DEATH);
|
||||
}
|
||||
|
||||
void ResetPhantomHandle() {
|
||||
void ResetPhantomHandle(HandleHolder handle_holder) {
|
||||
DCHECK(IsInUse());
|
||||
Address** handle = reinterpret_cast<Address**>(data_.parameter);
|
||||
*handle = nullptr;
|
||||
if (handle_holder == HandleHolder::kLive) {
|
||||
Address** handle = reinterpret_cast<Address**>(data_.parameter);
|
||||
*handle = nullptr;
|
||||
}
|
||||
NodeSpace<TracedNode>::Release(this);
|
||||
DCHECK(!IsInUse());
|
||||
}
|
||||
@ -652,6 +662,7 @@ class GlobalHandles::TracedNode final
|
||||
using NodeState = BitField8<State, 0, 2>;
|
||||
using IsInYoungList = NodeState::Next<bool, 1>;
|
||||
using IsRoot = IsInYoungList::Next<bool, 1>;
|
||||
using HasDestructor = IsRoot::Next<bool, 1>;
|
||||
|
||||
void ClearImplFields() {
|
||||
set_root(true);
|
||||
@ -690,18 +701,21 @@ Handle<Object> GlobalHandles::Create(Address value) {
|
||||
return Create(Object(value));
|
||||
}
|
||||
|
||||
Handle<Object> GlobalHandles::CreateTraced(Object value, Address* slot) {
|
||||
Handle<Object> GlobalHandles::CreateTraced(Object value, Address* slot,
|
||||
bool has_destructor) {
|
||||
GlobalHandles::TracedNode* result = traced_nodes_->Acquire(value);
|
||||
if (ObjectInYoungGeneration(value) && !result->is_in_young_list()) {
|
||||
traced_young_nodes_.push_back(result);
|
||||
result->set_in_young_list(true);
|
||||
}
|
||||
result->set_parameter(slot);
|
||||
result->set_has_destructor(has_destructor);
|
||||
return result->handle();
|
||||
}
|
||||
|
||||
Handle<Object> GlobalHandles::CreateTraced(Address value, Address* slot) {
|
||||
return CreateTraced(Object(value), slot);
|
||||
Handle<Object> GlobalHandles::CreateTraced(Address value, Address* slot,
|
||||
bool has_destructor) {
|
||||
return CreateTraced(Object(value), slot, has_destructor);
|
||||
}
|
||||
|
||||
Handle<Object> GlobalHandles::CopyGlobal(Address* location) {
|
||||
@ -808,7 +822,7 @@ void GlobalHandles::IterateWeakRootsForPhantomHandles(
|
||||
should_reset_handle(isolate()->heap(), node->location())) {
|
||||
if (node->IsPhantomResetHandle()) {
|
||||
node->MarkPending();
|
||||
node->ResetPhantomHandle();
|
||||
node->ResetPhantomHandle(HandleHolder::kLive);
|
||||
++number_of_phantom_handle_resets_;
|
||||
} else if (node->IsPhantomCallback()) {
|
||||
node->MarkPending();
|
||||
@ -820,7 +834,8 @@ void GlobalHandles::IterateWeakRootsForPhantomHandles(
|
||||
if (node->IsInUse() &&
|
||||
should_reset_handle(isolate()->heap(), node->location())) {
|
||||
if (node->IsPhantomResetHandle()) {
|
||||
node->ResetPhantomHandle();
|
||||
node->ResetPhantomHandle(node->has_destructor() ? HandleHolder::kLive
|
||||
: HandleHolder::kDead);
|
||||
++number_of_phantom_handle_resets_;
|
||||
} else {
|
||||
node->CollectPhantomCallbackData(&traced_pending_phantom_callbacks_);
|
||||
@ -906,7 +921,7 @@ void GlobalHandles::IterateYoungWeakUnmodifiedRootsForPhantomHandles(
|
||||
DCHECK(node->IsPhantomResetHandle() || node->IsPhantomCallback());
|
||||
if (node->IsPhantomResetHandle()) {
|
||||
node->MarkPending();
|
||||
node->ResetPhantomHandle();
|
||||
node->ResetPhantomHandle(HandleHolder::kLive);
|
||||
++number_of_phantom_handle_resets_;
|
||||
} else if (node->IsPhantomCallback()) {
|
||||
node->MarkPending();
|
||||
@ -921,6 +936,9 @@ void GlobalHandles::IterateYoungWeakUnmodifiedRootsForPhantomHandles(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LocalEmbedderHeapTracer* const tracer =
|
||||
isolate()->heap()->local_embedder_heap_tracer();
|
||||
for (TracedNode* node : traced_young_nodes_) {
|
||||
if (!node->IsInUse()) continue;
|
||||
|
||||
@ -928,7 +946,18 @@ void GlobalHandles::IterateYoungWeakUnmodifiedRootsForPhantomHandles(
|
||||
!should_reset_handle(isolate_->heap(), node->location()));
|
||||
if (should_reset_handle(isolate_->heap(), node->location())) {
|
||||
if (node->IsPhantomResetHandle()) {
|
||||
node->ResetPhantomHandle();
|
||||
if (node->has_destructor()) {
|
||||
// For handles with destructor it is guaranteed that the embedder
|
||||
// memory is still alive as the destructor would have otherwise
|
||||
// removed the memory.
|
||||
node->ResetPhantomHandle(HandleHolder::kLive);
|
||||
} else {
|
||||
v8::Value* value = ToApi<v8::Value>(node->handle());
|
||||
tracer->ResetHandleInNonTracingGC(
|
||||
*reinterpret_cast<v8::TracedGlobal<v8::Value>*>(&value));
|
||||
DCHECK(!node->IsInUse());
|
||||
}
|
||||
|
||||
++number_of_phantom_handle_resets_;
|
||||
} else {
|
||||
node->CollectPhantomCallbackData(&traced_pending_phantom_callbacks_);
|
||||
|
@ -101,8 +101,9 @@ class V8_EXPORT_PRIVATE GlobalHandles final {
|
||||
return Handle<T>::cast(Create(Object(value)));
|
||||
}
|
||||
|
||||
Handle<Object> CreateTraced(Object value, Address* slot);
|
||||
Handle<Object> CreateTraced(Address value, Address* slot);
|
||||
Handle<Object> CreateTraced(Object value, Address* slot, bool has_destructor);
|
||||
Handle<Object> CreateTraced(Address value, Address* slot,
|
||||
bool has_destructor);
|
||||
|
||||
void RecordStats(HeapStats* stats);
|
||||
|
||||
|
@ -57,6 +57,12 @@ class V8_EXPORT_PRIVATE LocalEmbedderHeapTracer final {
|
||||
bool IsRootForNonTracingGC(const v8::TracedGlobal<v8::Value>& handle) {
|
||||
return !InUse() || remote_tracer_->IsRootForNonTracingGC(handle);
|
||||
}
|
||||
void ResetHandleInNonTracingGC(const v8::TracedGlobal<v8::Value>& handle) {
|
||||
// Resetting is only called when IsRootForNonTracingGC returns false which
|
||||
// can only happen the EmbedderHeapTracer is set on API level.
|
||||
DCHECK(InUse());
|
||||
remote_tracer_->ResetHandleInNonTracingGC(handle);
|
||||
}
|
||||
|
||||
void NotifyV8MarkingWorklistWasEmpty() {
|
||||
num_v8_marking_worklist_was_empty_++;
|
||||
|
@ -631,6 +631,153 @@ TEST(TracedGlobalNoDestructor) {
|
||||
delete[] memory;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
class EmptyEmbedderHeapTracer : public v8::EmbedderHeapTracer {
|
||||
public:
|
||||
void RegisterV8References(
|
||||
const std::vector<std::pair<void*, void*>>& embedder_fields) final {}
|
||||
|
||||
bool AdvanceTracing(double deadline_in_ms) final { return true; }
|
||||
bool IsTracingDone() final { return true; }
|
||||
void TracePrologue(EmbedderHeapTracer::TraceFlags) final {}
|
||||
void TraceEpilogue() final {}
|
||||
void EnterFinalPause(EmbedderStackState) final {}
|
||||
};
|
||||
|
||||
// EmbedderHeapTracer that can optimize Scavenger handling when used with
|
||||
// TraceGlobal handles that have destructors.
|
||||
class EmbedderHeapTracerDestructorNonTracingClearing final
|
||||
: public EmptyEmbedderHeapTracer {
|
||||
public:
|
||||
explicit EmbedderHeapTracerDestructorNonTracingClearing(
|
||||
uint16_t class_id_to_optimize)
|
||||
: class_id_to_optimize_(class_id_to_optimize) {}
|
||||
|
||||
bool IsRootForNonTracingGC(const v8::TracedGlobal<v8::Value>& handle) final {
|
||||
return handle.WrapperClassId() != class_id_to_optimize_;
|
||||
}
|
||||
|
||||
void ResetHandleInNonTracingGC(
|
||||
const v8::TracedGlobal<v8::Value>& handle) final {
|
||||
// Not called when used with handles that have destructors.
|
||||
CHECK(false);
|
||||
}
|
||||
|
||||
private:
|
||||
uint16_t class_id_to_optimize_;
|
||||
};
|
||||
|
||||
// EmbedderHeapTracer that can optimize Scavenger handling when used with
|
||||
// TraceGlobal handles without destructors.
|
||||
class EmbedderHeapTracerNoDestructorNonTracingClearing final
|
||||
: public EmptyEmbedderHeapTracer {
|
||||
public:
|
||||
explicit EmbedderHeapTracerNoDestructorNonTracingClearing(
|
||||
uint16_t class_id_to_optimize)
|
||||
: class_id_to_optimize_(class_id_to_optimize) {}
|
||||
|
||||
bool IsRootForNonTracingGC(const v8::TracedGlobal<v8::Value>& handle) final {
|
||||
return handle.WrapperClassId() != class_id_to_optimize_;
|
||||
}
|
||||
|
||||
void ResetHandleInNonTracingGC(
|
||||
const v8::TracedGlobal<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.
|
||||
TracedGlobal<v8::Value>* original_handle =
|
||||
reinterpret_cast<TracedGlobal<v8::Value>*>(
|
||||
v8::Object::GetAlignedPointerFromInternalField(
|
||||
handle.As<v8::Object>(), 0));
|
||||
original_handle->Reset();
|
||||
}
|
||||
|
||||
private:
|
||||
uint16_t class_id_to_optimize_;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
void SetupOptimizedAndNonOptimizedHandle(
|
||||
v8::Isolate* isolate, uint16_t optimized_class_id,
|
||||
v8::TracedGlobal<T>* optimized_handle,
|
||||
v8::TracedGlobal<T>* non_optimized_handle) {
|
||||
v8::HandleScope scope(isolate);
|
||||
|
||||
v8::Local<v8::Object> optimized_object(ConstructTraceableJSApiObject(
|
||||
isolate->GetCurrentContext(), optimized_handle, nullptr));
|
||||
CHECK(optimized_handle->IsEmpty());
|
||||
*optimized_handle = v8::TracedGlobal<T>(isolate, optimized_object);
|
||||
CHECK(!optimized_handle->IsEmpty());
|
||||
optimized_handle->SetWrapperClassId(optimized_class_id);
|
||||
|
||||
v8::Local<v8::Object> non_optimized_object(ConstructTraceableJSApiObject(
|
||||
isolate->GetCurrentContext(), nullptr, nullptr));
|
||||
CHECK(non_optimized_handle->IsEmpty());
|
||||
*non_optimized_handle = v8::TracedGlobal<T>(isolate, non_optimized_object);
|
||||
CHECK(!non_optimized_handle->IsEmpty());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(TracedGlobalDestructorReclaimedOnScavenge) {
|
||||
ManualGCScope manual_gc;
|
||||
CcTest::InitializeVM();
|
||||
v8::Isolate* isolate = CcTest::isolate();
|
||||
v8::HandleScope scope(isolate);
|
||||
constexpr uint16_t kClassIdToOptimize = 17;
|
||||
EmbedderHeapTracerDestructorNonTracingClearing tracer(kClassIdToOptimize);
|
||||
heap::TemporaryEmbedderHeapTracerScope tracer_scope(isolate, &tracer);
|
||||
i::GlobalHandles* global_handles = CcTest::i_isolate()->global_handles();
|
||||
|
||||
static_assert(TracedGlobalTrait<
|
||||
v8::TracedGlobal<v8::Object>>::kRequiresExplicitDestruction,
|
||||
"destructor expected");
|
||||
const size_t initial_count = global_handles->handles_count();
|
||||
auto* optimized_handle = new v8::TracedGlobal<v8::Object>();
|
||||
auto* non_optimized_handle = new v8::TracedGlobal<v8::Object>();
|
||||
SetupOptimizedAndNonOptimizedHandle(isolate, kClassIdToOptimize,
|
||||
optimized_handle, non_optimized_handle);
|
||||
CHECK_EQ(initial_count + 2, global_handles->handles_count());
|
||||
heap::InvokeScavenge();
|
||||
CHECK_EQ(initial_count + 1, global_handles->handles_count());
|
||||
CHECK(optimized_handle->IsEmpty());
|
||||
delete optimized_handle;
|
||||
CHECK(!non_optimized_handle->IsEmpty());
|
||||
delete non_optimized_handle;
|
||||
CHECK_EQ(initial_count, global_handles->handles_count());
|
||||
}
|
||||
|
||||
TEST(TracedGlobalNoDestructorReclaimedOnScavenge) {
|
||||
ManualGCScope manual_gc;
|
||||
CcTest::InitializeVM();
|
||||
v8::Isolate* isolate = CcTest::isolate();
|
||||
v8::HandleScope scope(isolate);
|
||||
constexpr uint16_t kClassIdToOptimize = 23;
|
||||
EmbedderHeapTracerNoDestructorNonTracingClearing tracer(kClassIdToOptimize);
|
||||
heap::TemporaryEmbedderHeapTracerScope tracer_scope(isolate, &tracer);
|
||||
i::GlobalHandles* global_handles = CcTest::i_isolate()->global_handles();
|
||||
|
||||
static_assert(!TracedGlobalTrait<
|
||||
v8::TracedGlobal<v8::Value>>::kRequiresExplicitDestruction,
|
||||
"no destructor expected");
|
||||
const size_t initial_count = global_handles->handles_count();
|
||||
auto* optimized_handle = new v8::TracedGlobal<v8::Value>();
|
||||
auto* non_optimized_handle = new v8::TracedGlobal<v8::Value>();
|
||||
SetupOptimizedAndNonOptimizedHandle(isolate, kClassIdToOptimize,
|
||||
optimized_handle, non_optimized_handle);
|
||||
CHECK_EQ(initial_count + 2, global_handles->handles_count());
|
||||
heap::InvokeScavenge();
|
||||
CHECK_EQ(initial_count + 1, global_handles->handles_count());
|
||||
CHECK(optimized_handle->IsEmpty());
|
||||
delete optimized_handle;
|
||||
CHECK(!non_optimized_handle->IsEmpty());
|
||||
non_optimized_handle->Reset();
|
||||
delete non_optimized_handle;
|
||||
CHECK_EQ(initial_count, global_handles->handles_count());
|
||||
}
|
||||
|
||||
} // namespace heap
|
||||
} // namespace internal
|
||||
} // namespace v8
|
||||
|
Loading…
Reference in New Issue
Block a user