Skip to content

Commit 302865e

Browse files
addaleaxtargos
authored andcommitted
src: do not include partial AsyncWrap instances in heap dump
Heap dumps can be taken either through the inspector or the public API for it during an async_hooks init() hook, but at that point the AsyncWrap in question is not done initializing yet and virtual methods cannot be called on it. Address this issue (somewhat hackily) by excluding `AsyncWrap` instances which have not yet executed their `init()` hook fully from heap dumps. Fixes: #28786 PR-URL: #28789 Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: Rich Trott <[email protected]>
1 parent c0f24be commit 302865e

File tree

5 files changed

+86
-10
lines changed

5 files changed

+86
-10
lines changed

src/async_wrap.cc

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -577,22 +577,44 @@ AsyncWrap::AsyncWrap(Environment* env,
577577
ProviderType provider,
578578
double execution_async_id,
579579
bool silent)
580-
: BaseObject(env, object),
581-
provider_type_(provider) {
580+
: AsyncWrap(env, object) {
582581
CHECK_NE(provider, PROVIDER_NONE);
583-
CHECK_GE(object->InternalFieldCount(), 1);
582+
provider_type_ = provider;
584583

585584
// Use AsyncReset() call to execute the init() callbacks.
586585
AsyncReset(execution_async_id, silent);
586+
init_hook_ran_ = true;
587587
}
588588

589589
AsyncWrap::AsyncWrap(Environment* env, Local<Object> object)
590-
: BaseObject(env, object),
591-
provider_type_(PROVIDER_NONE) {
592-
CHECK_GE(object->InternalFieldCount(), 1);
590+
: BaseObject(env, object) {
591+
}
592+
593+
// This method is necessary to work around one specific problem:
594+
// Before the init() hook runs, if there is one, the BaseObject() constructor
595+
// registers this object with the Environment for finilization and debugging
596+
// purposes.
597+
// If the Environment decides to inspect this object for debugging, it tries to
598+
// call virtual methods on this object that are only (meaningfully) implemented
599+
// by the subclasses of AsyncWrap.
600+
// This could, with bad luck, happen during the AsyncWrap() constructor,
601+
// because we run JS code as part of it and that in turn can lead to a heapdump
602+
// being taken, either through the inspector or our programmatic API for it.
603+
// The object being initialized is not fully constructed at that point, and
604+
// in particular its virtual function table points to the AsyncWrap one
605+
// (as the subclass constructor has not yet begun execution at that point).
606+
// This means that the functions that are used for heap dump memory tracking
607+
// are not yet available, and trying to call them would crash the process.
608+
// We use this particular `IsDoneInitializing()` method to tell the Environment
609+
// that such debugging methods are not yet available.
610+
// This may be somewhat unreliable when it comes to future changes, because
611+
// at this point it *only* protects AsyncWrap subclasses, and *only* for cases
612+
// where heap dumps are being taken while the init() hook is on the call stack.
613+
// For now, it seems like the best solution, though.
614+
bool AsyncWrap::IsDoneInitializing() const {
615+
return init_hook_ran_;
593616
}
594617

595-
596618
AsyncWrap::~AsyncWrap() {
597619
EmitTraceEventDestroy();
598620
EmitDestroy();

src/async_wrap.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@ class AsyncWrap : public BaseObject {
210210
AsyncWrap* wrap_ = nullptr;
211211
};
212212

213+
bool IsDoneInitializing() const override;
214+
213215
private:
214216
friend class PromiseWrap;
215217

@@ -218,7 +220,8 @@ class AsyncWrap : public BaseObject {
218220
ProviderType provider,
219221
double execution_async_id,
220222
bool silent);
221-
ProviderType provider_type_;
223+
ProviderType provider_type_ = PROVIDER_NONE;
224+
bool init_hook_ran_ = false;
222225
// Because the values may be Reset(), cannot be made const.
223226
double async_id_ = kInvalidAsyncId;
224227
double trigger_async_id_;

src/base_object.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ class BaseObject : public MemoryRetainer {
8383
v8::Local<v8::Value> value,
8484
const v8::PropertyCallbackInfo<void>& info);
8585

86+
// This is a bit of a hack. See the override in async_wrap.cc for details.
87+
virtual bool IsDoneInitializing() const;
88+
8689
protected:
8790
// Can be used to avoid the automatic object deletion when the Environment
8891
// exits, for example when this object is owned and deleted by another

src/env.cc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -954,8 +954,8 @@ void MemoryTracker::TrackField(const char* edge_name,
954954
// identified and tracked here (based on their deleters),
955955
// but we may convert and track other known types here.
956956
BaseObject* obj = value.GetBaseObject();
957-
if (obj != nullptr) {
958-
this->TrackField("arg", obj);
957+
if (obj != nullptr && obj->IsDoneInitializing()) {
958+
TrackField("arg", obj);
959959
}
960960
CHECK_EQ(CurrentNode(), n);
961961
CHECK_NE(n->size_, 0);
@@ -1077,6 +1077,8 @@ void BaseObject::DeleteMe(void* data) {
10771077
delete self;
10781078
}
10791079

1080+
bool BaseObject::IsDoneInitializing() const { return true; }
1081+
10801082
Local<Object> BaseObject::WrappedObject() const {
10811083
return object();
10821084
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Flags: --expose-gc
2+
'use strict';
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const async_hooks = require('async_hooks');
6+
const v8 = require('v8');
7+
8+
// Regression test for https://github.com/nodejs/node/issues/28786
9+
// Make sure that creating a heap snapshot inside an async_hooks hook
10+
// works for Promises.
11+
12+
const createSnapshot = common.mustCall(() => {
13+
v8.getHeapSnapshot().resume();
14+
}, 8); // 2 × init + 2 × resolve + 1 × (after + before) + 2 × destroy = 8 calls
15+
16+
const promiseIds = [];
17+
18+
async_hooks.createHook({
19+
init(id, type) {
20+
if (type === 'PROMISE') {
21+
createSnapshot();
22+
promiseIds.push(id);
23+
}
24+
},
25+
26+
before(id) {
27+
if (promiseIds.includes(id)) createSnapshot();
28+
},
29+
30+
after(id) {
31+
if (promiseIds.includes(id)) createSnapshot();
32+
},
33+
34+
promiseResolve(id) {
35+
assert(promiseIds.includes(id));
36+
createSnapshot();
37+
},
38+
39+
destroy(id) {
40+
if (promiseIds.includes(id)) createSnapshot();
41+
}
42+
}).enable();
43+
44+
45+
Promise.resolve().then(() => {});
46+
setImmediate(global.gc);

0 commit comments

Comments
 (0)