Skip to content

Commit 40364b1

Browse files
committed
src: add check against non-weak BaseObjects at process exit
When a process exits cleanly, i.e. because the event loop ends up without things to wait for, the Node.js objects that are left on the heap should be: 1. weak, i.e. ready for garbage collection once no longer referenced, or 2. detached, i.e. scheduled for destruction once no longer referenced, or 3. an unrefed libuv handle, i.e. does not keep the event loop alive, or 4. an inactive libuv handle (essentially the same here) There are a few exceptions to this rule, but generally, if there are C++-backed Node.js objects on the heap that do not fall into the above categories, we may be looking at a potential memory leak. Most likely, the cause is a missing `MakeWeak()` call on the corresponding object. In order to avoid this kind of problem, we check the list of BaseObjects for these criteria. In this commit, we only do so when explicitly instructed to or when in debug mode (where --verify-base-objects is always-on). In particular, this avoids the kinds of memory leak issues that were fixed in the PRs referenced below. Refs: #35488 Refs: #35487 Refs: #35481 PR-URL: #35490 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Rich Trott <rtrott@gmail.com> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
1 parent bc0c094 commit 40364b1

18 files changed

+120
-16
lines changed

src/async_wrap.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ struct AsyncWrapObject : public AsyncWrap {
9999
return tmpl;
100100
}
101101

102+
bool IsNotIndicativeOfMemoryLeakAtExit() const override {
103+
// We can't really know what the underlying operation does. One of the
104+
// signs that it's time to remove this class. :)
105+
return true;
106+
}
107+
102108
SET_NO_MEMORY_INFO()
103109
SET_MEMORY_INFO_NAME(AsyncWrapObject)
104110
SET_SELF_SIZE(AsyncWrapObject)

src/base_object-inl.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,13 @@ void BaseObject::ClearWeak() {
146146
persistent_handle_.ClearWeak();
147147
}
148148

149+
bool BaseObject::IsWeakOrDetached() const {
150+
if (persistent_handle_.IsWeak()) return true;
151+
152+
if (!has_pointer_data()) return false;
153+
const PointerData* pd = const_cast<BaseObject*>(this)->pointer_data();
154+
return pd->wants_weak_jsobj || pd->is_detached;
155+
}
149156

150157
v8::Local<v8::FunctionTemplate>
151158
BaseObject::MakeLazilyInitializedJSTemplate(Environment* env) {

src/base_object.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ class BaseObject : public MemoryRetainer {
7878
// root and will not be touched by the garbage collector.
7979
inline void ClearWeak();
8080

81+
// Reports whether this BaseObject is using a weak reference or detached,
82+
// i.e. whether is can be deleted by GC once no strong BaseObjectPtrs refer
83+
// to it anymore.
84+
inline bool IsWeakOrDetached() const;
85+
8186
// Utility to create a FunctionTemplate with one internal field (used for
8287
// the `BaseObject*` pointer) and a constructor that initializes that field
8388
// to `nullptr`.
@@ -147,6 +152,10 @@ class BaseObject : public MemoryRetainer {
147152
virtual v8::Maybe<bool> FinalizeTransferRead(
148153
v8::Local<v8::Context> context, v8::ValueDeserializer* deserializer);
149154

155+
// Indicates whether this object is expected to use a strong reference during
156+
// a clean process exit (due to an empty event loop).
157+
virtual bool IsNotIndicativeOfMemoryLeakAtExit() const;
158+
150159
virtual inline void OnGCCollect();
151160

152161
private:

src/env.cc

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,22 +1194,43 @@ void Environment::RemoveUnmanagedFd(int fd) {
11941194
}
11951195
}
11961196

1197-
void Environment::ForEachBaseObject(BaseObjectIterator iterator) {
1197+
void Environment::PrintAllBaseObjects() {
11981198
size_t i = 0;
1199-
for (const auto& hook : cleanup_hooks_) {
1200-
BaseObject* obj = hook.GetBaseObject();
1201-
if (obj != nullptr) iterator(i, obj);
1202-
i++;
1203-
}
1204-
}
1205-
1206-
void PrintBaseObject(size_t i, BaseObject* obj) {
1207-
std::cout << "#" << i << " " << obj << ": " << obj->MemoryInfoName() << "\n";
1199+
std::cout << "BaseObjects\n";
1200+
ForEachBaseObject([&](BaseObject* obj) {
1201+
std::cout << "#" << i++ << " " << obj << ": " <<
1202+
obj->MemoryInfoName() << "\n";
1203+
});
12081204
}
12091205

1210-
void Environment::PrintAllBaseObjects() {
1211-
std::cout << "BaseObjects\n";
1212-
ForEachBaseObject(PrintBaseObject);
1206+
void Environment::VerifyNoStrongBaseObjects() {
1207+
// When a process exits cleanly, i.e. because the event loop ends up without
1208+
// things to wait for, the Node.js objects that are left on the heap should
1209+
// be:
1210+
//
1211+
// 1. weak, i.e. ready for garbage collection once no longer referenced, or
1212+
// 2. detached, i.e. scheduled for destruction once no longer referenced, or
1213+
// 3. an unrefed libuv handle, i.e. does not keep the event loop alive, or
1214+
// 4. an inactive libuv handle (essentially the same here)
1215+
//
1216+
// There are a few exceptions to this rule, but generally, if there are
1217+
// C++-backed Node.js objects on the heap that do not fall into the above
1218+
// categories, we may be looking at a potential memory leak. Most likely,
1219+
// the cause is a missing MakeWeak() call on the corresponding object.
1220+
//
1221+
// In order to avoid this kind of problem, we check the list of BaseObjects
1222+
// for these criteria. Currently, we only do so when explicitly instructed to
1223+
// or when in debug mode (where --verify-base-objects is always-on).
1224+
1225+
if (!options()->verify_base_objects) return;
1226+
1227+
ForEachBaseObject([this](BaseObject* obj) {
1228+
if (obj->IsNotIndicativeOfMemoryLeakAtExit()) return;
1229+
fprintf(stderr, "Found bad BaseObject during clean exit: %s\n",
1230+
obj->MemoryInfoName().c_str());
1231+
fflush(stderr);
1232+
ABORT();
1233+
});
12131234
}
12141235

12151236
EnvSerializeInfo Environment::Serialize(SnapshotCreator* creator) {
@@ -1473,4 +1494,8 @@ Local<FunctionTemplate> BaseObject::GetConstructorTemplate(Environment* env) {
14731494
return tmpl;
14741495
}
14751496

1497+
bool BaseObject::IsNotIndicativeOfMemoryLeakAtExit() const {
1498+
return IsWeakOrDetached();
1499+
}
1500+
14761501
} // namespace node

src/env.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -934,9 +934,8 @@ class Environment : public MemoryRetainer {
934934
void CreateProperties();
935935
void DeserializeProperties(const EnvSerializeInfo* info);
936936

937-
typedef void (*BaseObjectIterator)(size_t, BaseObject*);
938-
void ForEachBaseObject(BaseObjectIterator iterator);
939937
void PrintAllBaseObjects();
938+
void VerifyNoStrongBaseObjects();
940939
// Should be called before InitializeInspector()
941940
void InitializeDiagnostics();
942941
#if HAVE_INSPECTOR

src/handle_wrap.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@ void HandleWrap::OnGCCollect() {
8989
}
9090

9191

92+
bool HandleWrap::IsNotIndicativeOfMemoryLeakAtExit() const {
93+
return IsWeakOrDetached() ||
94+
!HandleWrap::HasRef(this) ||
95+
!uv_is_active(GetHandle());
96+
}
97+
98+
9299
void HandleWrap::MarkAsInitialized() {
93100
env()->handle_wrap_queue()->PushBack(this);
94101
state_ = kInitialized;

src/handle_wrap.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ class HandleWrap : public AsyncWrap {
8787
AsyncWrap::ProviderType provider);
8888
virtual void OnClose() {}
8989
void OnGCCollect() final;
90+
bool IsNotIndicativeOfMemoryLeakAtExit() const override;
9091

9192
void MarkAsInitialized();
9293
void MarkAsUninitialized();

src/inspector_js_api.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ class JSBindingsConnection : public AsyncWrap {
156156
SET_MEMORY_INFO_NAME(JSBindingsConnection)
157157
SET_SELF_SIZE(JSBindingsConnection)
158158

159+
bool IsNotIndicativeOfMemoryLeakAtExit() const override {
160+
return true; // Binding connections emit events on their own.
161+
}
162+
159163
private:
160164
std::unique_ptr<InspectorSession> session_;
161165
Global<Function> callback_;

src/module_wrap.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ class ModuleWrap : public BaseObject {
6060
SET_MEMORY_INFO_NAME(ModuleWrap)
6161
SET_SELF_SIZE(ModuleWrap)
6262

63+
bool IsNotIndicativeOfMemoryLeakAtExit() const override {
64+
// XXX: The garbage collection rules for ModuleWrap are *super* unclear.
65+
// Do these objects ever get GC'd? Are we just okay with leaking them?
66+
return true;
67+
}
68+
6369
private:
6470
ModuleWrap(Environment* env,
6571
v8::Local<v8::Object> object,

src/node_contextify.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ class CompiledFnEntry final : public BaseObject {
174174
v8::Local<v8::ScriptOrModule> script);
175175
~CompiledFnEntry();
176176

177+
bool IsNotIndicativeOfMemoryLeakAtExit() const override { return true; }
178+
177179
private:
178180
uint32_t id_;
179181
v8::Global<v8::ScriptOrModule> script_;

0 commit comments

Comments
 (0)