Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
async_hooks: use resource objects for Promises
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
addaleax committed Jun 8, 2017
commit a187daad7fc19f1526e865416da8b30ff206fb8d
90 changes: 82 additions & 8 deletions src/async-wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Copy link
Copy Markdown
Contributor

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

MakeWeak(this);

in this PR, since now we only keep the asyncId of parent promise here, is there any GC issue if we don't call MakeWeak ?

Copy link
Copy Markdown
Member Author

@addaleax addaleax Jun 4, 2017

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 MakeWeak call is being removed here, too: https://github.com/nodejs/node/pull/13452/files#diff-5e552c79e1538215f1621d1774852e71L315

The thing is, MakeWeak is 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 delete the C++ instance of PromiseWrap that 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.

Copy link
Copy Markdown
Contributor

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 MakeWeak API and node_object_wrap.h, and I will continue to learn about it.

And I am not quite understand is why we don't need the MakeWeak call before this version?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And I am not quite understand is why we don't need the MakeWeak call before this version?

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

Copy link
Copy Markdown
Contributor

@JiaLiPassion JiaLiPassion Jun 4, 2017

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!

Copy link
Copy Markdown
Contributor

@trevnorris trevnorris Jun 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@addaleax

The thing is, MakeWeak is 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... I'm to blame for this one. In fe2df3b I carefully went through to make sure Wrap() and ClearWrap() were called for all applicable instances forgetting that 3.5 years ago for whatever forsaken reason when I implemented BaseObject (d120d92) I made MakeWeak() automatically Wrap() the object. This was a horrible mistake on my part

Basically I would be happy if we removed the call to Wrap() in BaseObject::MakeWeak().

@JiaLiPassion

I just read MakeWeak API and node_object_wrap.h, and I will continue to learn about it.

Be careful. AsyncWrap doesn't use node_object_wrap. It uses base-object

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@trevnorris ,

Be careful. AsyncWrap doesn't use node_object_wrap. It uses base-object

thank you for pointing out this one.

}
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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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>();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 resource_object_value is an Object and then cast it to an Object.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.As<Object>() is not a cast in the JS sense of the word; you can read it as telling the engine “I’ve made sure that this is an object, now can I please use it as one?” (i.e. calling handle.As<T>() is undefined behaviour if handle is not actually a T)

Copy link
Copy Markdown
Member

@AndreasMadsen AndreasMadsen Jun 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, similar to reinterpret_cast.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.
}
Expand Down Expand Up @@ -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);
}
}


Expand Down
1 change: 1 addition & 0 deletions src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ namespace node {
V(pipe_constructor_template, v8::FunctionTemplate) \
V(process_object, v8::Object) \
V(promise_reject_function, v8::Function) \
V(promise_wrap_template, v8::ObjectTemplate) \
V(push_values_to_array_function, v8::Function) \
V(randombytes_constructor_template, v8::ObjectTemplate) \
V(script_context_constructor_template, v8::FunctionTemplate) \
Expand Down
25 changes: 25 additions & 0 deletions test/parallel/test-async-hooks-promise.js
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);