Skip to content

Commit 40364b1

Browse files
committedOct 7, 2020
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

+6
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

+7
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

+9
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

+38-13
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

+1-2
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

+7
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

+1
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

+4
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

+6
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

+2
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_;

‎src/node_http_parser.cc

+9
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,15 @@ class Parser : public AsyncWrap, public StreamListener {
880880
return HPE_PAUSED;
881881
}
882882

883+
884+
bool IsNotIndicativeOfMemoryLeakAtExit() const override {
885+
// HTTP parsers are able to emit events without any GC root referring
886+
// to them, because they receive events directly from the underlying
887+
// libuv resource.
888+
return true;
889+
}
890+
891+
883892
llhttp_t parser_;
884893
StringPtr fields_[kMaxHeaderFieldsCount]; // header fields
885894
StringPtr values_[kMaxHeaderFieldsCount]; // header values

‎src/node_main_instance.cc

+1
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ int NodeMainInstance::Run(const EnvSerializeInfo* env_info) {
168168
}
169169

170170
env->set_trace_sync_io(false);
171+
if (!env->is_stopping()) env->VerifyNoStrongBaseObjects();
171172
exit_code = EmitExit(env.get());
172173
}
173174

‎src/node_options.cc

+4
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
475475
"an error), 'warn' (enforce warnings) or 'none' (silence warnings)",
476476
&EnvironmentOptions::unhandled_rejections,
477477
kAllowedInEnvironment);
478+
AddOption("--verify-base-objects",
479+
"", /* undocumented, only for debugging */
480+
&EnvironmentOptions::verify_base_objects,
481+
kAllowedInEnvironment);
478482

479483
AddOption("--check",
480484
"syntax check script without executing",

‎src/node_options.h

+6
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,12 @@ class EnvironmentOptions : public Options {
150150
bool trace_warnings = false;
151151
std::string unhandled_rejections;
152152
std::string userland_loader;
153+
bool verify_base_objects =
154+
#ifdef DEBUG
155+
true;
156+
#else
157+
false;
158+
#endif // DEBUG
153159

154160
bool syntax_check_only = false;
155161
bool has_eval_string = false;

‎src/node_worker.cc

+9-1
Original file line numberDiff line numberDiff line change
@@ -362,8 +362,10 @@ void Worker::Run() {
362362
{
363363
int exit_code;
364364
bool stopped = is_stopped();
365-
if (!stopped)
365+
if (!stopped) {
366+
env_->VerifyNoStrongBaseObjects();
366367
exit_code = EmitExit(env_.get());
368+
}
367369
Mutex::ScopedLock lock(mutex_);
368370
if (exit_code_ == 0 && !stopped)
369371
exit_code_ = exit_code;
@@ -714,6 +716,12 @@ void Worker::MemoryInfo(MemoryTracker* tracker) const {
714716
tracker->TrackField("parent_port", parent_port_);
715717
}
716718

719+
bool Worker::IsNotIndicativeOfMemoryLeakAtExit() const {
720+
// Worker objects always stay alive as long as the child thread, regardless
721+
// of whether they are being referenced in the parent thread.
722+
return true;
723+
}
724+
717725
class WorkerHeapSnapshotTaker : public AsyncWrap {
718726
public:
719727
WorkerHeapSnapshotTaker(Environment* env, Local<Object> obj)

‎src/node_worker.h

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class Worker : public AsyncWrap {
5050
void MemoryInfo(MemoryTracker* tracker) const override;
5151
SET_MEMORY_INFO_NAME(Worker)
5252
SET_SELF_SIZE(Worker)
53+
bool IsNotIndicativeOfMemoryLeakAtExit() const override;
5354

5455
bool is_stopped() const;
5556

‎src/stream_base.h

+8
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,10 @@ class SimpleShutdownWrap : public ShutdownWrap, public OtherBase {
422422
SET_NO_MEMORY_INFO()
423423
SET_MEMORY_INFO_NAME(SimpleShutdownWrap)
424424
SET_SELF_SIZE(SimpleShutdownWrap)
425+
426+
bool IsNotIndicativeOfMemoryLeakAtExit() const override {
427+
return OtherBase::IsNotIndicativeOfMemoryLeakAtExit();
428+
}
425429
};
426430

427431
template <typename OtherBase>
@@ -435,6 +439,10 @@ class SimpleWriteWrap : public WriteWrap, public OtherBase {
435439
SET_NO_MEMORY_INFO()
436440
SET_MEMORY_INFO_NAME(SimpleWriteWrap)
437441
SET_SELF_SIZE(SimpleWriteWrap)
442+
443+
bool IsNotIndicativeOfMemoryLeakAtExit() const override {
444+
return OtherBase::IsNotIndicativeOfMemoryLeakAtExit();
445+
}
438446
};
439447

440448
} // namespace node

‎test/parallel/test-process-env-allowed-flags-are-documented.js

+1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ assert(undocumented.delete('--experimental-report'));
8888
assert(undocumented.delete('--experimental-worker'));
8989
assert(undocumented.delete('--no-node-snapshot'));
9090
assert(undocumented.delete('--loader'));
91+
assert(undocumented.delete('--verify-base-objects'));
9192

9293
assert.strictEqual(undocumented.size, 0,
9394
'The following options are not documented as allowed in ' +

0 commit comments

Comments
 (0)
Please sign in to comment.