Skip to content

Commit ed3648a

Browse files
committed
src: make CppgcMixin a MemoryRetainer
1 parent fa965bd commit ed3648a

14 files changed

+264
-91
lines changed

node.gyp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@
206206
'src/connect_wrap.h',
207207
'src/connection_wrap.h',
208208
'src/cppgc_helpers.h',
209+
'src/cppgc_helpers.cc',
209210
'src/dataqueue/queue.h',
210211
'src/debug_utils.h',
211212
'src/debug_utils-inl.h',

src/README.md

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,31 +1285,52 @@ staying alive in a closure). Long-lived cppgc objects can also
12851285
be held alive from C++ using persistent handles (see
12861286
`deps/v8/include/cppgc/persistent.h`) or as members of other living
12871287
cppgc-managed objects (see `deps/v8/include/cppgc/member.h`) if necessary.
1288-
Its destructor will be called when no other objects from the V8 heap reference
1289-
it, this can happen at any time after the garbage collector notices that
1290-
it's no longer reachable and before the V8 isolate is torn down.
1291-
See the [Oilpan documentation in Chromium][] for more details.
1292-
1293-
If the cppgc-managed objects does not need to perform any special cleanup,
1294-
it's fine to use the default destructor. If it needs to perform only trivial
1295-
cleanup that only affects its own members without calling into JS, potentially
1296-
triggering garbage collection or relying on a living `Environment`, then it's
1297-
fine to just implement the trivial cleanup in the destructor directly. If it
1298-
needs to do any substantial cleanup that involves a living `Environment`, because
1299-
the destructor can be called at any time by the garbage collection, even after
1300-
the `Environment` is already gone, it must implement the cleanup with this pattern:
13011288

1302-
```cpp
1303-
~MyWrap() { this->Clean(); }
1304-
void CleanEnvResource(Environment* env) override {
1289+
When a cppgc-managed object is no longer reachable in the heap, its destructor
1290+
will be invoked by the garbage collection, which can happen after the `Environment`
1291+
is already gone, or after any object it references is gone. It is therefore
1292+
unsafe to invoke V8 APIs directly in the destructors. To ensure safety,
1293+
the cleanups of a cppgc-managed object should adhere to different patterns,
1294+
depending on what it needs to do:
1295+
1296+
1. If it does not need to do any non-trivial cleanup, nor does its members, just use
1297+
the default destructor.
1298+
2. If the cleanup relies on a living `Environment`, but does not need to access V8
1299+
APIs, the class should use this pattern in its class body:
1300+
1301+
```cpp
1302+
~MyWrap() { this->Clean(); }
1303+
void CleanEnvResource(Environment* env) override {
13051304
// Do cleanup that relies on a living Environemnt. This would be
13061305
// called by CppgcMixin::Clean() first during Environment shutdown,
13071306
// while the Environment is still alive. If the destructor calls
13081307
// Clean() again later during garbage collection that happens after
13091308
// Environment shutdown, CleanEnvResource() would be skipped, preventing
13101309
// invalid access to the Environment.
1311-
}
1312-
```
1310+
}
1311+
```
1312+
3. If the cleanup relies on access to the V8 heap, including using any V8
1313+
handles, in addition to 2, it should use the `CPPGC_USING_PRE_FINALIZER`
1314+
macro (from the [`cppgc/prefinalizer.h` header][]) in the private
1315+
section of its class body:
1316+
1317+
```cpp
1318+
private:
1319+
CPPGC_USING_PRE_FINALIZER(MyWrap, Clean);
1320+
```
1321+
1322+
Both the destructor and the pre-finalizer are always called on the thread
1323+
in which the object is created.
1324+
1325+
It's worth noting that the use of pre-finalizers would have a negative impact
1326+
on the garbage collection performance as V8 needs to scan all of them during
1327+
each sweeping. If the object is expected to be created frequently in large
1328+
amounts in the application, it's better to avoid access to the V8 heap in its
1329+
cleanup to avoid having to use a pre-finalizer.
1330+
1331+
For more information about the cleanup of cppgc-managed objects and
1332+
what can be done in a pre-finalizer, see the [cppgc documentation][] and
1333+
the [`cppgc/prefinalizer.h` header][].
13131334

13141335
### Callback scopes
13151336

@@ -1436,6 +1457,7 @@ static void GetUserInfo(const FunctionCallbackInfo<Value>& args) {
14361457
[`async_hooks` module]: https://nodejs.org/api/async_hooks.html
14371458
[`async_wrap.h`]: async_wrap.h
14381459
[`base_object.h`]: base_object.h
1460+
[`cppgc/prefinalizer.h` header]: ../deps/v8/include/cppgc/prefinalizer.h
14391461
[`handle_wrap.h`]: handle_wrap.h
14401462
[`memory_tracker.h`]: memory_tracker.h
14411463
[`req_wrap.h`]: req_wrap.h
@@ -1446,6 +1468,7 @@ static void GetUserInfo(const FunctionCallbackInfo<Value>& args) {
14461468
[`vm` module]: https://nodejs.org/api/vm.html
14471469
[binding function]: #binding-functions
14481470
[cleanup hooks]: #cleanup-hooks
1471+
[cppgc documentation]: ../deps/v8/include/cppgc/README.md
14491472
[event loop]: #event-loop
14501473
[exception handling]: #exception-handling
14511474
[fast API calls]: ../doc/contributing/adding-v8-fast-api.md

src/base_object.cc

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,5 +181,4 @@ void BaseObjectList::MemoryInfo(node::MemoryTracker* tracker) const {
181181
if (bo->IsDoneInitializing()) tracker->Track(bo);
182182
}
183183
}
184-
185184
} // namespace node

src/cppgc_helpers-inl.h

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#include "cppgc_helpers.h"
2+
#include "env-inl.h"
3+
4+
namespace node {
5+
6+
template <typename T>
7+
void CppgcMixin::Wrap(T* ptr, Realm* realm, v8::Local<v8::Object> obj) {
8+
CHECK_GE(obj->InternalFieldCount(), T::kInternalFieldCount);
9+
ptr->realm_ = realm;
10+
v8::Isolate* isolate = realm->isolate();
11+
ptr->traced_reference_ = v8::TracedReference<v8::Object>(isolate, obj);
12+
// Note that ptr must be of concrete type T in Wrap.
13+
v8::Object::Wrap<v8::CppHeapPointerTag::kDefaultTag>(isolate, obj, ptr);
14+
// Keep the layout consistent with BaseObjects.
15+
obj->SetAlignedPointerInInternalField(
16+
kEmbedderType, realm->isolate_data()->embedder_id_for_cppgc());
17+
obj->SetAlignedPointerInInternalField(kSlot, ptr);
18+
realm->TrackCppgcWrapper(ptr);
19+
}
20+
21+
template <typename T>
22+
void CppgcMixin::Wrap(T* ptr, Environment* env, v8::Local<v8::Object> obj) {
23+
Wrap(ptr, env->principal_realm(), obj);
24+
}
25+
26+
template <typename T>
27+
T* CppgcMixin::Unwrap(v8::Local<v8::Object> obj) {
28+
// We are not using v8::Object::Unwrap currently because that requires
29+
// access to isolate which the ASSIGN_OR_RETURN_UNWRAP macro that we'll shim
30+
// with ASSIGN_OR_RETURN_UNWRAP_GC doesn't take, and we also want a
31+
// signature consistent with BaseObject::Unwrap() to avoid churn. Since
32+
// cppgc-managed objects share the same layout as BaseObjects, just unwrap
33+
// from the pointer in the internal field, which should be valid as long as
34+
// the object is still alive.
35+
if (obj->InternalFieldCount() != T::kInternalFieldCount) {
36+
return nullptr;
37+
}
38+
T* ptr = static_cast<T*>(obj->GetAlignedPointerFromInternalField(T::kSlot));
39+
return ptr;
40+
}
41+
42+
v8::Local<v8::Object> CppgcMixin::object() const {
43+
return traced_reference_.Get(realm_->isolate());
44+
}
45+
46+
Environment* CppgcMixin::env() const {
47+
return realm_->env();
48+
}
49+
50+
} // namespace node

src/cppgc_helpers.cc

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#include "cppgc_helpers.h"
2+
#include "env-inl.h"
3+
4+
namespace node {
5+
6+
void CppgcWrapperList::Cleanup() {
7+
for (auto handle : *this) {
8+
handle->Clean();
9+
}
10+
}
11+
12+
void CppgcWrapperList::MemoryInfo(MemoryTracker* tracker) const {
13+
for (auto handle : *this) {
14+
tracker->Track(handle);
15+
}
16+
}
17+
18+
} // namespace node

src/cppgc_helpers.h

Lines changed: 36 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@
66
#include <type_traits> // std::remove_reference
77
#include "cppgc/garbage-collected.h"
88
#include "cppgc/name-provider.h"
9-
#include "env.h"
109
#include "memory_tracker.h"
10+
#include "util.h"
1111
#include "v8-cppgc.h"
1212
#include "v8-sandbox.h"
1313
#include "v8.h"
1414

1515
namespace node {
1616

17+
class Environment;
18+
class Realm;
19+
class CppgcWrapperList;
20+
1721
/**
1822
* This is a helper mixin with a BaseObject-like interface to help
1923
* implementing wrapper objects managed by V8's cppgc (Oilpan) library.
@@ -47,8 +51,7 @@ namespace node {
4751
* // Do cleanup that relies on a living Environemnt.
4852
* }
4953
*/
50-
class CppgcMixin : public cppgc::GarbageCollectedMixin,
51-
public CppgcWrapperListNode {
54+
class CppgcMixin : public cppgc::GarbageCollectedMixin, public MemoryRetainer {
5255
public:
5356
// To help various callbacks access wrapper objects with different memory
5457
// management, cppgc-managed objects share the same layout as BaseObjects.
@@ -58,71 +61,55 @@ class CppgcMixin : public cppgc::GarbageCollectedMixin,
5861
// invoked from the child class constructor, per cppgc::GarbageCollectedMixin
5962
// rules.
6063
template <typename T>
61-
static void Wrap(T* ptr, Environment* env, v8::Local<v8::Object> obj) {
62-
CHECK_GE(obj->InternalFieldCount(), T::kInternalFieldCount);
63-
ptr->env_ = env;
64-
v8::Isolate* isolate = env->isolate();
65-
ptr->traced_reference_ = v8::TracedReference<v8::Object>(isolate, obj);
66-
v8::Object::Wrap<v8::CppHeapPointerTag::kDefaultTag>(isolate, obj, ptr);
67-
// Keep the layout consistent with BaseObjects.
68-
obj->SetAlignedPointerInInternalField(
69-
kEmbedderType, env->isolate_data()->embedder_id_for_cppgc());
70-
obj->SetAlignedPointerInInternalField(kSlot, ptr);
71-
env->cppgc_wrapper_list()->PushFront(ptr);
72-
}
64+
static inline void Wrap(T* ptr, Realm* realm, v8::Local<v8::Object> obj);
65+
template <typename T>
66+
static inline void Wrap(T* ptr, Environment* env, v8::Local<v8::Object> obj);
7367

74-
v8::Local<v8::Object> object() const {
75-
return traced_reference_.Get(env_->isolate());
68+
inline v8::Local<v8::Object> object() const;
69+
inline Environment* env() const;
70+
inline v8::Local<v8::Object> object(v8::Isolate* isolate) const {
71+
return traced_reference_.Get(isolate);
7672
}
7773

78-
Environment* env() const { return env_; }
79-
8074
template <typename T>
81-
static T* Unwrap(v8::Local<v8::Object> obj) {
82-
// We are not using v8::Object::Unwrap currently because that requires
83-
// access to isolate which the ASSIGN_OR_RETURN_UNWRAP macro that we'll shim
84-
// with ASSIGN_OR_RETURN_UNWRAP_GC doesn't take, and we also want a
85-
// signature consistent with BaseObject::Unwrap() to avoid churn. Since
86-
// cppgc-managed objects share the same layout as BaseObjects, just unwrap
87-
// from the pointer in the internal field, which should be valid as long as
88-
// the object is still alive.
89-
if (obj->InternalFieldCount() != T::kInternalFieldCount) {
90-
return nullptr;
91-
}
92-
T* ptr = static_cast<T*>(obj->GetAlignedPointerFromInternalField(T::kSlot));
93-
return ptr;
94-
}
75+
static inline T* Unwrap(v8::Local<v8::Object> obj);
9576

9677
// Subclasses are expected to invoke CppgcMixin::Trace() in their own Trace()
9778
// methods.
9879
void Trace(cppgc::Visitor* visitor) const override {
9980
visitor->Trace(traced_reference_);
10081
}
10182

102-
// This implements CppgcWrapperListNode::Clean and is run for all the
103-
// remaining Cppgc wrappers tracked in the Environment during Environment
104-
// shutdown. The destruction of the wrappers would happen later, when the
105-
// final garbage collection is triggered when CppHeap is torn down as part of
106-
// the Isolate teardown. If subclasses of CppgcMixin wish to perform cleanups
107-
// that depend on the Environment during destruction, they should implment it
108-
// in a CleanEnvResource() override, and then call this->Clean() from their
83+
// TODO(joyeecheung): use ObjectSizeTrait;
84+
inline size_t SelfSize() const override { return sizeof(*this); }
85+
inline bool IsCppgcWrapper() const override { return true; }
86+
87+
// This is run for all the remaining Cppgc wrappers tracked in the Realm
88+
// during Realm shutdown. The destruction of the wrappers would happen later,
89+
// when the final garbage collection is triggered when CppHeap is torn down as
90+
// part of the Isolate teardown. If subclasses of CppgcMixin wish to perform
91+
// cleanups that depend on the Realm during destruction, they should implment
92+
// it in a CleanEnvResource() override, and then call this->Clean() from their
10993
// destructor. Outside of CleanEnvResource(), subclasses should avoid calling
11094
// into JavaScript or perform any operation that can trigger garbage
11195
// collection during the destruction.
112-
void Clean() override {
113-
if (env_ == nullptr) return;
114-
this->CleanEnvResource(env_);
115-
env_ = nullptr;
96+
void Clean() {
97+
if (realm_ == nullptr) return;
98+
this->CleanEnvResource(realm_);
99+
realm_ = nullptr;
116100
}
117101

118102
// The default implementation of CleanEnvResource() is a no-op. Subclasses
119-
// should override it to perform cleanup that require a living Environment,
103+
// should override it to perform cleanup that require a living Realm,
120104
// instead of doing these cleanups directly in the destructor.
121-
virtual void CleanEnvResource(Environment* env) {}
105+
virtual void CleanEnvResource(Realm* realm) {}
106+
107+
friend class CppgcWrapperList;
122108

123109
private:
124-
Environment* env_ = nullptr;
110+
Realm* realm_ = nullptr;
125111
v8::TracedReference<v8::Object> traced_reference_;
112+
ListNode<CppgcMixin> wrapper_list_node_;
126113
};
127114

128115
// If the class doesn't have additional owned traceable data, use this macro to
@@ -137,7 +124,8 @@ class CppgcMixin : public cppgc::GarbageCollectedMixin,
137124
#define SET_CPPGC_NAME(Klass) \
138125
inline const char* GetHumanReadableName() const final { \
139126
return "Node / " #Klass; \
140-
}
127+
} \
128+
inline const char* MemoryInfoName() const override { return #Klass; }
141129

142130
/**
143131
* Similar to ASSIGN_OR_RETURN_UNWRAP() but works on cppgc-managed types

src/env.cc

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,8 +1302,6 @@ void Environment::RunCleanup() {
13021302
CleanupHandles();
13031303
}
13041304

1305-
for (CppgcWrapperListNode* handle : cppgc_wrapper_list_) handle->Clean();
1306-
13071305
for (const int fd : unmanaged_fds_) {
13081306
uv_fs_t close_req;
13091307
uv_fs_close(nullptr, &close_req, fd, nullptr);

src/env.h

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -602,12 +602,6 @@ class Cleanable {
602602
friend class Environment;
603603
};
604604

605-
class CppgcWrapperListNode {
606-
public:
607-
virtual void Clean() = 0;
608-
ListNode<CppgcWrapperListNode> wrapper_list_node;
609-
};
610-
611605
/**
612606
* Environment is a per-isolate data structure that represents an execution
613607
* environment. Each environment has a principal realm. An environment can
@@ -903,18 +897,12 @@ class Environment final : public MemoryRetainer {
903897
typedef ListHead<HandleWrap, &HandleWrap::handle_wrap_queue_> HandleWrapQueue;
904898
typedef ListHead<ReqWrapBase, &ReqWrapBase::req_wrap_queue_> ReqWrapQueue;
905899
typedef ListHead<Cleanable, &Cleanable::cleanable_queue_> CleanableQueue;
906-
typedef ListHead<CppgcWrapperListNode,
907-
&CppgcWrapperListNode::wrapper_list_node>
908-
CppgcWrapperList;
909900

910901
inline HandleWrapQueue* handle_wrap_queue() { return &handle_wrap_queue_; }
911902
inline CleanableQueue* cleanable_queue() {
912903
return &cleanable_queue_;
913904
}
914905
inline ReqWrapQueue* req_wrap_queue() { return &req_wrap_queue_; }
915-
inline CppgcWrapperList* cppgc_wrapper_list() {
916-
return &cppgc_wrapper_list_;
917-
}
918906

919907
// https://w3c.github.io/hr-time/#dfn-time-origin
920908
inline uint64_t time_origin() {
@@ -1203,7 +1191,6 @@ class Environment final : public MemoryRetainer {
12031191
CleanableQueue cleanable_queue_;
12041192
HandleWrapQueue handle_wrap_queue_;
12051193
ReqWrapQueue req_wrap_queue_;
1206-
CppgcWrapperList cppgc_wrapper_list_;
12071194

12081195
int handle_cleanup_waiting_ = 0;
12091196
int request_waiting_ = 0;

0 commit comments

Comments
 (0)