Skip to content
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

async_hooks: use resource objects for Promises #13452

Closed
wants to merge 5 commits into from

Conversation

addaleax
Copy link
Member

@addaleax addaleax commented Jun 4, 2017

As discussed in #13367. /cc @nodejs/async_hooks

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.

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • documentation is changed or added
  • commit message follows commit guidelines
Affected core subsystem(s)

async_hooks

@nodejs-github-bot nodejs-github-bot added async_wrap c++ Issues and PRs that require attention from people who are familiar with C++. labels Jun 4, 2017
@addaleax addaleax added async_hooks Issues and PRs related to the async hooks subsystem. promises Issues and PRs related to ECMAScript promises. labels Jun 4, 2017
@AndreasMadsen
Copy link
Member

AndreasMadsen commented Jun 4, 2017

Thanks for looking at this.

Do we not risk getting a very long chain of PromiseWrap objects that prevents all the promises in the chain from being easily garbage collected?

I was thinking it might be better to use the asyncId of the parent, rather than the PromiseWrap itself. If the user want to walk the .parent chain they can just store the information in a Map.

There is also the problem of resource.parent.parent being misleading if the PromiseHooks were enabled late. By returning the parent id it will always be correct and if the user enables PromiseHooks late they just don't have the information to walk the .parent chain, which is a reasonable consequence.

/cc @JiaLiPassion

@AndreasMadsen
Copy link
Member

AndreasMadsen commented Jun 4, 2017

Also, cc @Fishrock123 who was the first to request this.

@JiaLiPassion
Copy link
Contributor

JiaLiPassion commented Jun 4, 2017

@addaleax , thank you for implementing this.

@AndreasMadsen , are you suggestion to pass the asyncId of the parent promise in init hook like below?

const parent_promise = new Promise((resolve, reject) => {resolve(5);}); 
const promise = parent_promise.then((val) => {return val;});

function init(id, type, triggerId, handle) {
    process._rawDebug(`id: ${id}, type: ${type}, triggerId: ${triggerId}, 
                                        handle.promise: ${handle.promise}, 
                                        handle.parentid: ${handle.parentId}`);
}

the output will look like

id: 2, type: PROMISE, triggerId: 1, handle.promise: {status: 'pending'}, handle.parentid: undefined
id: 3, type: PROMISE, triggerId: 2, handle.promise: {status: 'pending'}, handle.parentid: 2

And if application want the promise object, they can keep a asyncId and promise map
in the application memory themselves.
Is my understanding correct?

@AndreasMadsen
Copy link
Member

Is my understanding correct?

Yes. That is exactly what I mean.

@JiaLiPassion
Copy link
Contributor

@AndreasMadsen , got it! I still try to understand the C++ code,
If this can make GC easier to control, I agree this is a less-risky solution.

@addaleax
Copy link
Member Author

addaleax commented Jun 4, 2017

@AndreasMadsen @JiaLiPassion Updated!

I still try to understand the C++ code,

If you have any questions, please ask them, here or in the diff – whatever you want to know, a lot of other people will also like to know.

@JiaLiPassion
Copy link
Contributor

If you have any questions, please ask them, here or in the diff – whatever you want to know, a lot of other people will also like to know.

@addaleax , got it!

promise_wrap_template->SetAccessor(
FIXED_ONE_BYTE_STRING(env->isolate(), "parentId"),
PromiseWrap::GetParentId);
env->SetProtoMethod(ctor, "getAsyncId", AsyncWrap::GetAsyncId);
Copy link
Member

Choose a reason for hiding this comment

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

What is the cost of this? If there is any I don't think we should add it. The result is given as the first parameter in init anyway and we generally don't document that getAsyncId exists.

Copy link
Member Author

Choose a reason for hiding this comment

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

I thought it’s nice to have for consistency, but removing it is trivial. I’ve removed it for now, we can always revisit if necessary.

I haven’t measured, but I can’t really imagine it’s costly – it adds a method to the prototype, which should be practically free.

Local<Object> resource_object;
PromiseWrap* wrap = nullptr;
if (resource_object_value->IsObject()) {
resource_object = resource_object_value.As<Object>();
Copy link
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
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
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
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.

Copy link
Member

@AndreasMadsen AndreasMadsen left a comment

Choose a reason for hiding this comment

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

Code LGTM. Since zone.js will be dependent on this, I think should document the PromiseWrap resource object.

@addaleax
Copy link
Member Author

addaleax commented Jun 4, 2017

@AndreasMadsen I’ve added some documentation… PTAL

@@ -203,13 +203,16 @@ UDPSENDWRAP, UDPWRAP, WRITEWRAP, ZLIB, SSLCONNECTION, PBKDF2REQUEST,
RANDOMBYTESREQUEST, TLSWRAP
```

There is also the `PROMISE` resource type, which is used to track `Promise`
Copy link
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.

Maybe we should add PROMISE to the provider list in async_wrap.

Copy link
Member Author

Choose a reason for hiding this comment

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

I’ve kept it out of it because it doesn’t quite qualify as “provided by the built-in Node.js modules”, but if you think it’s better, I can just drop this sentence and reword the existing text a bit

Copy link
Member

Choose a reason for hiding this comment

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

No just leave it out for now. @trevnorris suggested that maybe we should add all types to the Providers list. See #13287 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

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

Hm. Seems we don't even include Timer or TickObject in that list. Or even as a side comment.

Anyway, only argument I'd have for adding it to the above list is because it shows up in process.binding('async_wrap').Providers. But that's not a strong argument. I'll leave this up to you.

Copy link
Member

Choose a reason for hiding this comment

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

I think we should delete the list. The user shouldn't really on a defined list of types because the Embedder API allows for extra types.

Copy link
Contributor

Choose a reason for hiding this comment

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

@AndreasMadsen The reason I decided to pass the "type" (history note: using Provider was not the name I wanted to use, but switched long ago b/c of pressure to change on the PR) was for to allow filtering resources. I'd like to have a list of all "core types" (which would include TickObject and Timer) and I'd expect module authors to document their own "types".

Though because this relies a bit on internals could we put a note like "subject to change in future major (or minor?) releases without deprecation"?

Copy link
Member

Choose a reason for hiding this comment

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

We already have a note saying resource objects are not to be trusted:

... The API for getting this information is currently not considered public ...

I guess we can be more specific.

@@ -282,37 +285,87 @@ 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
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
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
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
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
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
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
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.

@AndreasMadsen AndreasMadsen added the semver-minor PRs that contain new features and should be released in the next minor version. label Jun 7, 2017
Copy link
Member

@AndreasMadsen AndreasMadsen left a comment

Choose a reason for hiding this comment

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

LGTM.

I set it to semver-minor since this is an API change, although technically we make less than experimental guarantees about the resource object, so an argument could be made for semver-patch.

I think we should remove the type list. The user shouldn't really on a defined list of types because the Embedder API allows for extra types. However, it is not related to the PR.

@refack
Copy link
Contributor

refack commented Jun 7, 2017

I set it to semver-minor since this is an API change, although technically we make less than experimental guarantees about the resource object, so an argument could be made for semver-patch.

I think we should remove the type list. The user shouldn't really on a defined list of types because the Embedder API allows for extra types. However, it is not related to the PR.

👍

(@AndreasMadsen If you put comments in the "review summary" I can't 👍 them)

@AndreasMadsen
Copy link
Member

AndreasMadsen commented Jun 7, 2017

(@AndreasMadsen If you put comments in the "review summary" I can't 👍 them)

I know. I prefer it this way :D evil laughter

@addaleax
Copy link
Member Author

addaleax commented Jun 7, 2017


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
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>();

addaleax added 5 commits June 8, 2017 20:34
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.
@addaleax addaleax force-pushed the promise-wrap-resource branch from ec1c0d4 to 4acaed3 Compare June 8, 2017 18:36
@addaleax
Copy link
Member Author

addaleax commented Jun 8, 2017

Copy link
Contributor

@trevnorris trevnorris left a comment

Choose a reason for hiding this comment

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

Thank you much

@addaleax
Copy link
Member Author

addaleax commented Jun 8, 2017

Landed in 8f39881

@addaleax addaleax closed this Jun 8, 2017
@addaleax addaleax deleted the promise-wrap-resource branch June 8, 2017 22:01
addaleax added a commit that referenced this pull request Jun 8, 2017
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.

PR-URL: #13452
Reviewed-By: Andreas Madsen <[email protected]>
Reviewed-By: Trevor Norris <[email protected]>
addaleax added a commit to addaleax/node that referenced this pull request Jun 10, 2017
@addaleax addaleax mentioned this pull request Jun 10, 2017
jasnell pushed a commit that referenced this pull request Jun 13, 2017
PR-URL: #13561
Ref: #13452
Reviewed-By: Refael Ackermann <[email protected]>
Reviewed-By: Trevor Norris <[email protected]>
Reviewed-By: James M Snell <[email protected]>
addaleax added a commit that referenced this pull request Jun 17, 2017
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.

PR-URL: #13452
Reviewed-By: Andreas Madsen <[email protected]>
Reviewed-By: Trevor Norris <[email protected]>
addaleax added a commit that referenced this pull request Jun 17, 2017
PR-URL: #13561
Ref: #13452
Reviewed-By: Refael Ackermann <[email protected]>
Reviewed-By: Trevor Norris <[email protected]>
Reviewed-By: James M Snell <[email protected]>
@addaleax addaleax mentioned this pull request Jun 17, 2017
addaleax added a commit that referenced this pull request Jun 21, 2017
PR-URL: #13561
Ref: #13452
Reviewed-By: Refael Ackermann <[email protected]>
Reviewed-By: Trevor Norris <[email protected]>
Reviewed-By: James M Snell <[email protected]>
addaleax added a commit that referenced this pull request Jun 21, 2017
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.

PR-URL: #13452
Reviewed-By: Andreas Madsen <[email protected]>
Reviewed-By: Trevor Norris <[email protected]>
addaleax added a commit that referenced this pull request Jun 24, 2017
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.

PR-URL: #13452
Reviewed-By: Andreas Madsen <[email protected]>
Reviewed-By: Trevor Norris <[email protected]>
rvagg pushed a commit that referenced this pull request Jun 29, 2017
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.

PR-URL: #13452
Reviewed-By: Andreas Madsen <[email protected]>
Reviewed-By: Trevor Norris <[email protected]>
addaleax added a commit that referenced this pull request Jul 11, 2017
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.

PR-URL: #13452
Reviewed-By: Andreas Madsen <[email protected]>
Reviewed-By: Trevor Norris <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
async_hooks Issues and PRs related to the async hooks subsystem. c++ Issues and PRs that require attention from people who are familiar with C++. promises Issues and PRs related to ECMAScript promises. semver-minor PRs that contain new features and should be released in the next minor version.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants