-
-
Notifications
You must be signed in to change notification settings - Fork 35.4k
async_hooks: use resource objects for Promises #13452
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
a187daa
0875682
464d2af
2652910
4acaed3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
Use `PromiseWrap` resource objects whose lifetimes are tied to the `Promise` instances themselves to track promises, and have a `.promise` getter that points to the `Promise` and a `.parent` property that points to the parent Promise’s resource object, if there is any. The properties are implemented as getters for internal fields rather than normal properties in the hope that it helps keep performance for the common case that async_hooks users will often not inspect them.
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,7 @@ using v8::Context; | |
| using v8::Float64Array; | ||
| using v8::Function; | ||
| using v8::FunctionCallbackInfo; | ||
| using v8::FunctionTemplate; | ||
| using v8::HandleScope; | ||
| using v8::HeapProfiler; | ||
| using v8::Integer; | ||
|
|
@@ -44,8 +45,10 @@ using v8::Local; | |
| using v8::MaybeLocal; | ||
| using v8::Number; | ||
| using v8::Object; | ||
| using v8::ObjectTemplate; | ||
| using v8::Promise; | ||
| using v8::PromiseHookType; | ||
| using v8::PropertyCallbackInfo; | ||
| using v8::RetainedObjectInfo; | ||
| using v8::String; | ||
| using v8::Symbol; | ||
|
|
@@ -282,37 +285,91 @@ bool AsyncWrap::EmitAfter(Environment* env, double async_id) { | |
| class PromiseWrap : public AsyncWrap { | ||
| public: | ||
| PromiseWrap(Environment* env, Local<Object> object, bool silent) | ||
| : AsyncWrap(env, object, PROVIDER_PROMISE, silent) {} | ||
| : AsyncWrap(env, object, PROVIDER_PROMISE, silent) { | ||
| MakeWeak(this); | ||
| } | ||
| size_t self_size() const override { return sizeof(*this); } | ||
|
|
||
| static constexpr int kPromiseField = 1; | ||
| static constexpr int kParentField = 2; | ||
| static constexpr int kInternalFieldCount = 3; | ||
|
|
||
| static PromiseWrap* New(Environment* env, | ||
| Local<Promise> promise, | ||
| PromiseWrap* parent_wrap, | ||
| bool silent); | ||
| static void GetPromise(Local<String> property, | ||
| const PropertyCallbackInfo<Value>& info); | ||
| static void GetParent(Local<String> property, | ||
| const PropertyCallbackInfo<Value>& info); | ||
| }; | ||
|
|
||
| PromiseWrap* PromiseWrap::New(Environment* env, | ||
| Local<Promise> promise, | ||
| PromiseWrap* parent_wrap, | ||
| bool silent) { | ||
| Local<Object> object = env->promise_wrap_template() | ||
| ->NewInstance(env->context()).ToLocalChecked(); | ||
| object->SetInternalField(PromiseWrap::kPromiseField, promise); | ||
| object->SetAlignedPointerInInternalField( | ||
| PromiseWrap::kParentField, | ||
| static_cast<void*>(parent_wrap)); | ||
| CHECK_EQ(promise->GetAlignedPointerFromInternalField(0), nullptr); | ||
| promise->SetInternalField(0, object); | ||
| return new PromiseWrap(env, object, silent); | ||
| } | ||
|
|
||
| void PromiseWrap::GetPromise(Local<String> property, | ||
| const PropertyCallbackInfo<Value>& info) { | ||
| info.GetReturnValue().Set(info.Holder()->GetInternalField(kPromiseField)); | ||
| } | ||
|
|
||
| void PromiseWrap::GetParent(Local<String> property, | ||
| const PropertyCallbackInfo<Value>& info) { | ||
| Isolate* isolate = info.GetIsolate(); | ||
| PromiseWrap* parent = static_cast<PromiseWrap*>( | ||
| info.Holder()->GetAlignedPointerFromInternalField(kParentField)); | ||
| if (parent == nullptr) | ||
| info.GetReturnValue().Set(Undefined(isolate)); | ||
| else | ||
| info.GetReturnValue().Set(parent->object()); | ||
| } | ||
|
|
||
| static void PromiseHook(PromiseHookType type, Local<Promise> promise, | ||
| Local<Value> parent, void* arg) { | ||
| Local<Context> context = promise->CreationContext(); | ||
| Environment* env = Environment::GetCurrent(context); | ||
| PromiseWrap* wrap = Unwrap<PromiseWrap>(promise); | ||
| Local<Value> resource_object_value = promise->GetInternalField(0); | ||
| Local<Object> resource_object; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: this is only used in the if statement below so mind just making it: Local<Object> resource_object = resource_object_value.As<Object>(); |
||
| PromiseWrap* wrap = nullptr; | ||
| if (resource_object_value->IsObject()) { | ||
| resource_object = resource_object_value.As<Object>(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My V8-C++ knowledge might be lacking, but why do we check that
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, similar to
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In addition a Debug build it asserts, so instead of simply crashing b/c it was cast to the wrong type it'll actually say you've screwed up. |
||
| wrap = Unwrap<PromiseWrap>(resource_object); | ||
| } | ||
| if (type == PromiseHookType::kInit || wrap == nullptr) { | ||
| bool silent = type != PromiseHookType::kInit; | ||
| PromiseWrap* parent_wrap = nullptr; | ||
|
|
||
| // set parent promise's async Id as this promise's triggerId | ||
| if (parent->IsPromise()) { | ||
| // parent promise exists, current promise | ||
| // is a chained promise, so we set parent promise's id as | ||
| // current promise's triggerId | ||
| Local<Promise> parent_promise = parent.As<Promise>(); | ||
| auto parent_wrap = Unwrap<PromiseWrap>(parent_promise); | ||
| Local<Value> parent_resource = parent_promise->GetInternalField(0); | ||
| if (parent_resource->IsObject()) { | ||
| parent_wrap = Unwrap<PromiseWrap>(parent_resource.As<Object>()); | ||
| } | ||
|
|
||
| if (parent_wrap == nullptr) { | ||
| // create a new PromiseWrap for parent promise with silent parameter | ||
| parent_wrap = new PromiseWrap(env, parent_promise, true); | ||
| parent_wrap->MakeWeak(parent_wrap); | ||
| parent_wrap = PromiseWrap::New(env, parent_promise, nullptr, true); | ||
| } | ||
| // get id from parentWrap | ||
| double trigger_id = parent_wrap->get_id(); | ||
| env->set_init_trigger_id(trigger_id); | ||
| } | ||
| wrap = new PromiseWrap(env, promise, silent); | ||
| wrap->MakeWeak(wrap); | ||
|
|
||
| wrap = PromiseWrap::New(env, promise, parent_wrap, silent); | ||
| } else if (type == PromiseHookType::kResolve) { | ||
| // TODO(matthewloring): need to expose this through the async hooks api. | ||
| } | ||
|
|
@@ -351,6 +408,23 @@ static void SetupHooks(const FunctionCallbackInfo<Value>& args) { | |
| SET_HOOK_FN(destroy); | ||
| env->AddPromiseHook(PromiseHook, nullptr); | ||
| #undef SET_HOOK_FN | ||
|
|
||
| { | ||
| Local<FunctionTemplate> ctor = | ||
| FunctionTemplate::New(env->isolate()); | ||
| ctor->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "PromiseWrap")); | ||
| Local<ObjectTemplate> promise_wrap_template = ctor->InstanceTemplate(); | ||
| promise_wrap_template->SetInternalFieldCount( | ||
| PromiseWrap::kInternalFieldCount); | ||
| promise_wrap_template->SetAccessor( | ||
| FIXED_ONE_BYTE_STRING(env->isolate(), "promise"), | ||
| PromiseWrap::GetPromise); | ||
| promise_wrap_template->SetAccessor( | ||
| FIXED_ONE_BYTE_STRING(env->isolate(), "parent"), | ||
| PromiseWrap::GetParent); | ||
| env->SetProtoMethod(ctor, "getAsyncId", AsyncWrap::GetAsyncId); | ||
| env->set_promise_wrap_template(promise_wrap_template); | ||
| } | ||
| } | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| 'use strict'; | ||
| const common = require('../common'); | ||
| const assert = require('assert'); | ||
| const async_hooks = require('async_hooks'); | ||
|
|
||
| const initCalls = []; | ||
|
|
||
| async_hooks.createHook({ | ||
| init: common.mustCall((id, type, triggerId, resource) => { | ||
| assert.strictEqual(type, 'PROMISE'); | ||
| initCalls.push({id, triggerId, resource}); | ||
| }, 2) | ||
| }).enable(); | ||
|
|
||
| const a = Promise.resolve(42); | ||
| const b = a.then(common.mustCall()); | ||
|
|
||
| assert.strictEqual(initCalls[0].triggerId, 1); | ||
| assert.strictEqual(initCalls[0].resource.parent, undefined); | ||
| assert.strictEqual(initCalls[0].resource.promise, a); | ||
| assert.strictEqual(initCalls[0].resource.getAsyncId(), initCalls[0].id); | ||
| assert.strictEqual(initCalls[1].triggerId, initCalls[0].id); | ||
| assert.strictEqual(initCalls[1].resource.parent, initCalls[0].resource); | ||
| assert.strictEqual(initCalls[1].resource.promise, b); | ||
| assert.strictEqual(initCalls[1].resource.getAsyncId(), initCalls[1].id); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@addaleax , sorry for very basic question, why we add
in this PR, since now we only keep the
asyncIdof parent promise here, is there any GC issue if we don't callMakeWeak?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This question is not basic – it’s something I didn’t quite figure out until a couple days ago myself. And I’m not sure whether you’ve seen it, but the current
MakeWeakcall is being removed here, too: https://github.com/nodejs/node/pull/13452/files#diff-5e552c79e1538215f1621d1774852e71L315The thing is,
MakeWeakis a not the best choice of name. What it actually does is to set the 0th internal field of the associated object to the passed pointer, and then mark the handled contained in the Wrap object as weak and set a destroy callback for V8 to call.And it needs to then make handle weak, because otherwise all V8 knows is that there is a persistent handle to the object; it doesn’t know how we use it, so it can’t garbage collect it. By making it weak, we tell V8 to destroy it once there are no other (non-weak) handles left, and to run our registered destroy callback (which will
deletethe C++ instance ofPromiseWrapthat we are using).I agree, it’s all a bit tricky to see through. I’m mostly moving it here because that helps with avoiding duplication, and because that’s how we use it elsewhere in the code as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@addaleax , thank you for the explanation. I just read
MakeWeakAPI andnode_object_wrap.h, and I will continue to learn about it.And I am not quite understand is why we don't need the
MakeWeakcall before this version?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was there, it was just called after the constructor instead of being a part of it: https://github.com/nodejs/node/pull/13452/files#diff-5e552c79e1538215f1621d1774852e71L315
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@addaleax , oh, yes, it was there, thanks. Currently I have no further questions!
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@addaleax
And... I'm to blame for this one. In fe2df3b I carefully went through to make sure
Wrap()andClearWrap()were called for all applicable instances forgetting that 3.5 years ago for whatever forsaken reason when I implementedBaseObject(d120d92) I madeMakeWeak()automaticallyWrap()the object. This was a horrible mistake on my partBasically I would be happy if we removed the call to
Wrap()inBaseObject::MakeWeak().@JiaLiPassion
Be careful. AsyncWrap doesn't use
node_object_wrap. It usesbase-objectThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@trevnorris ,
thank you for pointing out this one.