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_wrap,src: promise hook integration #13000

Closed
wants to merge 16 commits into from
Closed

async_wrap,src: promise hook integration #13000

wants to merge 16 commits into from

Conversation

matthewloring
Copy link

@matthewloring matthewloring commented May 12, 2017

This change provides unified tracking of asynchronous promise lifecycles
for both domains and async hooks.

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_wrap,src

This change provides unified tracking of asynchronous promise lifecycles
for both domains and 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 May 12, 2017
@matthewloring
Copy link
Author

Here is the work I have so far on promise integrations for domains/async hooks. There are a few outstanding things I wanted to discuss:

  1. Currently promises do not have an internal field slot that can be used by BaseObject. To get around this, we create a separate object stored on the promise that can be wrapped and delegate domain lookups to that object. Maybe @gsathya has a better approach for this or could comment on the feasibility of having an internal field added to the promise object template.

  2. It would be good to surface additional information that promise hooks makes available through async hooks. Specifically, promise hooks gives information about parent-child relationships when promises are constructed as part of a chain. It would be nice to pass metadata to the async hook callbacks that could relay this (or maybe there is already a pattern in place for making this available). Also, some diagnostic tools (specifically for resource tracking) are interested in knowing when promises resolve in addition to the "before" and "after" events. Maybe we could consider adding a resolution async hook event (potentially this could be used by other resource types other than just promises). I wrote up some more thoughts on this here.

  3. It would be good to get a review from someone with more understanding of test-async-wrap=-getasyncid.js to figure out the right way of exposing the promise wrap object to JS.

@Fishrock123
Copy link
Contributor

My quick thoughts on (2.) is that it would probably be better have the returned Resource Object be it's own abstracted information object rather than the Promise itself. It can still have the actual Promise as a property or getter if necessary.

@@ -201,6 +332,7 @@ static void SetupHooks(const FunctionCallbackInfo<Value>& args) {
SET_HOOK_FN(before);
SET_HOOK_FN(after);
SET_HOOK_FN(destroy);
env->isolate()->SetPromiseHook(PromiseHook);
Copy link
Member

Choose a reason for hiding this comment

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

If I'm reading this correct, this means that we only have domain support for promises when async_wrap is enabled. That might be okay if we enable async_wrap when domain is used, but as it is now this makes the behavior of domain depend on the state of async_hooks.

Copy link
Member

Choose a reason for hiding this comment

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

Ack, please use AddPromiseHook instead. :)

Copy link
Author

Choose a reason for hiding this comment

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

Updated to use AddPromiseHook.

Local<Context> context = info.GetIsolate()->GetCurrentContext();
Environment* env = Environment::GetCurrent(context);
info.GetReturnValue().Set(Object::Cast(*info.Data())->Get(context,
env->domain_string()).ToLocalChecked());
Copy link
Member

Choose a reason for hiding this comment

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

The alignment is off here, and we don’t usually dereference handles unless there’s a reason to. This should work:

  info.GetReturnValue().Set(
      info.Data().As<Object>()->Get(context,
                                    env->domain_string()).ToLocalChecked());

Copy link
Author

Choose a reason for hiding this comment

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

Thanks!

Local<Object> obj = tem->NewInstance(context).ToLocalChecked();
PromiseWrap* wrap = new PromiseWrap(env, obj);
promise->DefineOwnProperty(context,
FIXED_ONE_BYTE_STRING(env->isolate(), async_id_key),
Copy link
Member

Choose a reason for hiding this comment

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

Instead of using FIXED_ONE_BYTE_STRING, you may want to add to the string list in env.h (to avoid creating strings over and over again)

Copy link
Author

Choose a reason for hiding this comment

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

Updated.

@@ -201,6 +332,7 @@ static void SetupHooks(const FunctionCallbackInfo<Value>& args) {
SET_HOOK_FN(before);
SET_HOOK_FN(after);
SET_HOOK_FN(destroy);
env->isolate()->SetPromiseHook(PromiseHook);
Copy link
Member

Choose a reason for hiding this comment

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

Ack, please use AddPromiseHook instead. :)

src/node.cc Outdated
void AddPromiseHook(v8::Isolate* isolate, promise_hook_func fn, void* arg) {
Environment* env = Environment::GetCurrent(isolate);
env->AddPromiseHook(fn, arg);
}
Copy link
Member

Choose a reason for hiding this comment

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

This should stay, it was intentionally requested for the public API so that Node using the single PromiseHook slot doesn’t block others from doing the same.

Choose a reason for hiding this comment

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

If this lands in Node 8, then at least our need for AddPromiseHook is gone. It is only if these promise hooks don't make it into async_wrap in Node 8 as publicly documented features that we needed to share the promise hook.

Copy link
Author

Choose a reason for hiding this comment

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

I removed a little more than intended. This has been added back in.

@@ -66,6 +66,12 @@ function testInitialized(req, ctor_name) {
}


{
// TODO: determine the rigth way to expose promise wrap.
new Promise((res) => res(5));
Copy link
Member

@AndreasMadsen AndreasMadsen May 12, 2017

Choose a reason for hiding this comment

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

I don't think we need to expose the PromiseWrap on the new Promise object. PromiseWrap just has to be the resource object in the init hook, and the PromiseWrap object has to point to the new Promise object.

We don't expose *Wrap objects to userland in any other way than through the async_hooks resource object.

Copy link
Author

Choose a reason for hiding this comment

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

So does this mean that we do not need to expose a "getAsyncId" operation for promises (adding it will have performance implications so it would be great if it isn't needed)?

Copy link
Member

Choose a reason for hiding this comment

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

Correct. I don't see any reason for getAsyncId to be exposed on promises.

Copy link
Author

Choose a reason for hiding this comment

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

Updated the comment to reflect this.

GetPromiseDomain, NULL, obj).FromJust();
}
} else if (type == PromiseHookType::kResolve) {
// TODO(matthewloring): need to expose this through the async hooks api.
Copy link
Member

Choose a reason for hiding this comment

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

kResolve only happens once per promise, when the promise is resolved, right? In that case, I think we can just put the current async id that caused the resolve on the PromiseWrap object. That should provide most of the additional information.

Yes, there is also the actual event but that information can be inferred when kBefore is emitted. In case .then() is never called, I don't this it is that interesting when the promise is resolved since it is never used.

Copy link
Member

Choose a reason for hiding this comment

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

kResolve only happens once per promise

yep :)

when the promise is resolved, right?

You’d think so, wouldn’t you? 😄 This isn’t called when the promise is resolved, it’s at the beginning of when the resolve function (as in new Promise((resolve) => …)) or some equivalent of it is called. That might not be when the Promise is resolved, though (because you can call resolve() with other pending promises).

I’m not actually sure kResolve is a very interesting thing for async hooks?

Choose a reason for hiding this comment

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

I'm not sure if a promise's then correlates with kResolve, but a promise's then can be called multiple times.

Copy link
Member

@AndreasMadsen AndreasMadsen May 12, 2017

Choose a reason for hiding this comment

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

This isn’t called when the promise is resolved, it’s at the beginning of when the resolve function (as in new Promise((resolve) => …)) or some equivalent of it is called.

Haha, that was actually what I meant, when resolve() is called. I always mix up my promise terminology.

Copy link
Member

@AndreasMadsen AndreasMadsen May 12, 2017

Choose a reason for hiding this comment

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

I’m not actually sure kResolve is a very interesting thing for async hooks?

This was perhaps the most discussed thing in the async_hooks EPS. This comment descibes the two viewpoints nodejs/node-eps#18 (comment), though I don't really agree with the "who needs what" part (stack-trace vs CLS) after talking with APM providers (@watson).

I think adding the async id that called resolve() to the PromiseWrap is the best option for satisfying both parties.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is an example scenario that almost captures all the possibilities that Promises and async hooks can interact:

(function runner() {
  const p = new Promise(function p_fn0(res) {
    setImmediate(function si0() {
      res('foo')
    });
  }).then(function then0(val) {
    return Fn(val);
  });

  setImmediate(function si0() {
    p.catch(function catch0(err) {
      printLongErrorStack(err);  // not defined
    });
  });

  function Fn(val) {
    if (val === 'foo') throw new Error('bam');
  }
})();

If each Promise executor, onFulfilled or onRejected only collect the stack when new Promise() or .then() runs then the stack would be:

    at printLongErrorStack
    at catch0
    at si0
    at runner

Though it would be helpful to produce the following long stack:

    at printLongErrorStack
    at catch0
    at Fn
    at then0
    at si0
    at p_fn0
    at runner

I've had some lengthy discussions about this and, though I don't remember with whom, I recall agreeing that the logical place to execute the AsyncEvent hooks would produce the first call stack; but it should also be possible to produce the second call stack (@matthewloring was this conversation with you?).

So for this PR the first stack above is expected, but we should also address how to produce the second. IIRC the API in in place to allow watching for resolve or reject calls. At which point a stack can be produced then (somehow?) attached to the Promise handle that's responsible for the current execution scope.

Copy link
Author

Choose a reason for hiding this comment

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

Yup, this conversation was with me and I think it captures the concern with attaching the async id to the promise resource at resolution time so that it can be read in kBefore. This approach does not allow async_hook users to "react" to a promise resolution (state cannot be observed/updated at resolution time). Would it be too much to add another async_hook type for the resolution of async work (promise or otherwise)? With the addition of the C++ embedder api I can imagine that other types of async work could notify when async operations completes even if the JS callback isn't invoked immediately.

Copy link
Contributor

Choose a reason for hiding this comment

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

@matthewloring Adding another type that fires at a different time wouldn't be a problem.

Copy link
Member

Choose a reason for hiding this comment

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

This approach does not allow async_hook users to "react" to a promise resolution (state cannot be observed/updated at resolution time). Would it be too much to add another async_hook type for the resolution of async work (promise or otherwise)? With the addition of the C++ embedder api I can imagine that other types of async work could notify when async operations completes even if the JS callback isn't invoked immediately.

While it is not a technical issue to add another type (I assume you mean event type and not resource type), I think it would be better to do in a future PR where we have a better chance to evaluate the needs for such event type for other cases than promises.

So far the only concern mentioned in the EPS has been regarding context across async boundaries, which "attaching the async id to the promise resource" does solve, although not in a particularly nice way.

Copy link
Author

Choose a reason for hiding this comment

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

Addressing this in a follow on seems fine to me.

@gsathya
Copy link
Member

gsathya commented May 15, 2017

Maybe @gsathya has a better approach for this or could comment on the feasibility of having an internal field added to the promise object template.

The current approach seems fine to me. You can use a private symbol if you don't think this should be exposed.

LGTM. Thanks for implementing this!

Environment* env = Environment::GetCurrent(context);
const char async_id_key[] = "__async_wrap";
const char tag_id_key[] = "__async_wrap_tag";
if (type == PromiseHookType::kInit) {
Copy link
Author

Choose a reason for hiding this comment

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

Is there an analogue of env->in_domain() for async_hooks? It would be good to bail out here if we know async hooks is not enabled.

Copy link
Member

@addaleax addaleax May 16, 2017

Choose a reason for hiding this comment

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

@matthewloring What Trevor has been doing elsewhere in the code is testing env()->async_hooks()->fields()[AsyncHooks::kInit] == 0, I think that should work.

I still like the AddPromiseHook approach, though – it helps with keeping domains and async hooks separate, and more importantly I think we’d only want to set a promise hook if we’re actually consuming the information. Otherwise it seems like something that might come with unnecessary performance impact on Promise usage. Seems like that has already been addressed, sorry.

Copy link
Author

Choose a reason for hiding this comment

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

@addaleax As far as I can tell, the line above is only used to avoid invoking init hooks if none have been registered. I was hoping to avoid attaching the new async hook related properties to promises in the case where async hooks were disabled. Adding these properties causes ~30x increase in the time it takes to construct/resolve promises (adding the property causes all of V8's map checks to fail and prevent promises from ever running on fast paths). I am hoping to fix this by adding an internal field to promises (https://codereview.chromium.org/2889863002/) but it would be good to avoid modifying promises if async hooks are not in use.

This slow down may also come in to play when domains are active as that also seems to modify promise shape but I haven't run any benchmarks against domains yet.

Copy link
Member

Choose a reason for hiding this comment

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

@matthewloring I think this might be enough for what you want? I didn’t try it with this PR but it passes testing on its own.

diff
diff --git a/lib/async_hooks.js b/lib/async_hooks.js
index 867b5eb52da1..49f8fa5becf2 100644
--- a/lib/async_hooks.js
+++ b/lib/async_hooks.js
@@ -49,12 +49,7 @@ const before_symbol = Symbol('before');
 const after_symbol = Symbol('after');
 const destroy_symbol = Symbol('destroy');
 
-// Setup the callbacks that node::AsyncWrap will call when there are hooks to
-// process. They use the same functions as the JS embedder API.
-async_wrap.setupHooks({ init,
-                        before: emitBeforeN,
-                        after: emitAfterN,
-                        destroy: emitDestroyN });
+let setupHooksCalled = false;
 
 // Used to fatally abort the process if a callback throws.
 function fatalError(e) {
@@ -103,6 +98,16 @@ class AsyncHook {
     if (hooks_array.includes(this))
       return;
 
+    if (!setupHooksCalled) {
+      setupHooksCalled = true;
+      // Setup the callbacks that node::AsyncWrap will call when there are
+      // hooks to process. They use the same functions as the JS embedder API.
+      async_wrap.setupHooks({ init,
+                              before: emitBeforeN,
+                              after: emitAfterN,
+                              destroy: emitDestroyN });
+    }
+
     // createHook() has already enforced that the callbacks are all functions,
     // so here simply increment the count of whether each callbacks exists or
     // not.

Copy link
Member

@AndreasMadsen AndreasMadsen May 17, 2017

Choose a reason for hiding this comment

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

I was hoping to avoid attaching the new async hook related properties to promises in the case where async hooks were disabled.

How much will inverting the dependencies help, as suggested in #13000 (comment)? Personally, I see no reason to add properties to the promise object.

Copy link
Member

@AndreasMadsen AndreasMadsen May 17, 2017

Choose a reason for hiding this comment

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

@addaleax yes, but can a WeakMap (wrap = map[promise]) not be used to get around setting properties?

Copy link
Member

Choose a reason for hiding this comment

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

@AndreasMadsen As I’ve recently learned, no :( http://iamstef.net/n/shapeshifting.html

Copy link
Member

Choose a reason for hiding this comment

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

I see, that is sad :(

Copy link
Contributor

Choose a reason for hiding this comment

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

@matthewloring I actually had a call like this previously, but removed it b/c it wasn't needed anywhere in the initial PR. Basically if kInit == !(kBefore == !(kAfter == !(kDestroy == 0))) then the hooks list is empty. I had consolidated this into kTotalHooks that was the sum of all active hooks, so the only necessary check was kTotalHooks == 0. I can re-add this if necessary easily. Already have the code from the previous implementation.

Copy link
Author

Choose a reason for hiding this comment

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

Just noting that @addaleax's suggestion above has been pulled to here to separate the discussion.

@joshgav
Copy link
Contributor

joshgav commented May 16, 2017

@matthewloring

Specifically, promise hooks gives information about parent-child relationships when promises are constructed as part of a chain. It would be nice to pass metadata to the async hook callbacks that could relay this (or maybe there is already a pattern in place for making this available).

I think trigger_id is meant to serve as the parent context ID. It's made available to init hooks here and seems that it should be available as the second parameter to before hooks (see e.g. here); but the before hook implementation of trigger_id seems incomplete here.

Copy link
Member

@addaleax addaleax left a comment

Choose a reason for hiding this comment

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

Looks mostly good. I’d have a preference for https://github.com/nodejs/node/pull/13000/files#r117075597 to be used here, but I can also PR that separately if you prefer.

Local<Context> context = promise->CreationContext();
Environment* env = Environment::GetCurrent(context);
const char async_id_key[] = "__async_wrap";
const char tag_id_key[] = "__async_wrap_tag";
Copy link
Member

Choose a reason for hiding this comment

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

These two seem to be unused now?

Copy link
Author

Choose a reason for hiding this comment

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

These were removed in 0afbcfc. Thanks.

Local<v8::Value> external_wrap =
promise->Get(context, env->promise_async_id()).ToLocalChecked();
PromiseWrap* wrap =
static_cast<PromiseWrap*>(v8::External::Cast(*external_wrap)->Value());
Copy link
Member

Choose a reason for hiding this comment

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

Getting a déjà-vu, maybe I already commented before, but we generally prefer not to dereference handles explicitly (i.e. using external_wrap.As<v8::External>()->Value() is fine)

Copy link
Contributor

Choose a reason for hiding this comment

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

@addaleax Don't they basically result in the same operation? e.g. from class Local:

  template <class S>
  V8_INLINE Local<S> As() const {
    return Local<S>::Cast(*this);
  }

@matthewloring To satiate my paranoia, mind adding a CHECK_NE(wrap, nullptr) just below?

Copy link
Member

Choose a reason for hiding this comment

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

@trevnorris Yes, sorry for not being clear; they result in the same compiled code. This was just a style thing, the overwhelming majority of our codebase uses .As<>(), that’s all.

Copy link
Author

Choose a reason for hiding this comment

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

@addaleax Added the check here 34ee31f

src/node.cc Outdated
@@ -1208,8 +1155,6 @@ void SetupDomainUse(const FunctionCallbackInfo<Value>& args) {
Local<ArrayBuffer> array_buffer =
ArrayBuffer::New(env->isolate(), fields, sizeof(*fields) * fields_count);

env->AddPromiseHook(DomainPromiseHook, static_cast<void*>(env));
Copy link
Member

Choose a reason for hiding this comment

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

Is this being removed intentionally?

obj->Set(context, env->domain_string(),
env->domain_array()->Get(context, 0).ToLocalChecked()).FromJust();
promise->SetAccessor(context, env->domain_string(),
GetPromiseDomain, NULL, obj).FromJust();
Copy link
Member

Choose a reason for hiding this comment

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

Is there anything speaking against removing the domain handling inside this promise hook and re-adding the DomainPromiseHook? That way we don’t have to pay for domains or async hooks if we don’t use them

Copy link
Author

Choose a reason for hiding this comment

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

My goal with the refactor was to try and have the domain entry and exit logic + promise hook before/after logic live in one place to avoid having complex parallel code in multiple places. I think this can be achieved with re-adding DomainPromiseHook (it is also highly unlikely that a project would use both domains and async hooks so keeping them logically separated would be good).

Copy link
Member

Choose a reason for hiding this comment

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

Yea, I get what you were aiming at, and I do like the idea a lot. But I’d just rather prefer not to have domain usage depend on using async hooks, that’s all; I don’t care that much how that’s done.

(I.e. don’t assume I’d think the solution I pushed here is optimal – I’m sure there’s room for improvement)

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.

Great work here. Have a few things, and let me know if you'd like an assist.

if (enter_v->IsFunction()) {
if (enter_v.As<Function>()->Call(domain, 0, nullptr).IsEmpty()) {
FatalError("node::AsyncWrap::MakeCallback",
"domain enter callback threw, please report this");
Copy link
Contributor

Choose a reason for hiding this comment

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

style nit: align arguments

Copy link
Author

Choose a reason for hiding this comment

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

@addaleax Fixed this here 5717d75.

if (ar.IsEmpty()) {
ClearFatalExceptionHandlers(wrap->env());
FatalException(wrap->env()->isolate(), try_catch);
return false;
Copy link
Contributor

Choose a reason for hiding this comment

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

FatalException() always exits the application in this case because ClearFatalException() removed all unhandledException callbacks. So I'll assume this return is so the compiler doesn't emit a warning.

I decided this would be the safest course of action when first implementing AsyncWrap because it was difficult to properly clean up if an async hook threw. It was difficult enough to properly cleanup if a user's callback threw (see process._fatalException() in lib/internal/bootstrap_node.js). If we can properly show it's safe to recover from an async hook throwing then I'm all for changing this behavior.

Copy link
Author

Choose a reason for hiding this comment

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

That makes sense, maybe we can look into that more in a follow on pr.

// Unfortunately, promises don't have internal fields. Need a surrogate that
// async wrap can wrap.
Local<v8::ObjectTemplate> tem = v8::ObjectTemplate::New(env->isolate());
tem->SetInternalFieldCount(1);
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be faster to create an Eternal<ObjectTemplate> (e.g. adding this to ENVIRONMENT_STRONG_PERSISTENT_PROPERTIES() in src/env.h) that can be reused for all NewInstance().

Copy link
Author

Choose a reason for hiding this comment

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

The need for a custom object template will be going away as soon as we move to an internal property on promises (blocked on #13175). I can modify this but I intend to get the promise internal property refactoring in before 8.0.

Copy link
Author

Choose a reason for hiding this comment

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

I've added this for now.

v8::PropertyAttribute::DontEnum).FromJust();
promise->DefineOwnProperty(context,
env->promise_async_tag(),
obj, v8::PropertyAttribute::DontEnum).FromJust();
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want the user to be able to alter these? In src/async-wrap.cc I used

  v8::PropertyAttribute ReadOnlyDontDelete =
      static_cast<v8::PropertyAttribute>(v8::ReadOnly | v8::DontDelete);

Though I probably should have added DontEnum to that list.

Copy link
Author

Choose a reason for hiding this comment

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

This property will be going away as soon as we move to an internal property on promises (blocked on #13175). I can modify this but I intend to get the promise internal property refactoring in before 8.0.

Copy link
Author

Choose a reason for hiding this comment

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

I've added this for now.

GetPromiseDomain, NULL, obj).FromJust();
}
} else if (type == PromiseHookType::kResolve) {
// TODO(matthewloring): need to expose this through the async hooks api.
Copy link
Contributor

Choose a reason for hiding this comment

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

This is an example scenario that almost captures all the possibilities that Promises and async hooks can interact:

(function runner() {
  const p = new Promise(function p_fn0(res) {
    setImmediate(function si0() {
      res('foo')
    });
  }).then(function then0(val) {
    return Fn(val);
  });

  setImmediate(function si0() {
    p.catch(function catch0(err) {
      printLongErrorStack(err);  // not defined
    });
  });

  function Fn(val) {
    if (val === 'foo') throw new Error('bam');
  }
})();

If each Promise executor, onFulfilled or onRejected only collect the stack when new Promise() or .then() runs then the stack would be:

    at printLongErrorStack
    at catch0
    at si0
    at runner

Though it would be helpful to produce the following long stack:

    at printLongErrorStack
    at catch0
    at Fn
    at then0
    at si0
    at p_fn0
    at runner

I've had some lengthy discussions about this and, though I don't remember with whom, I recall agreeing that the logical place to execute the AsyncEvent hooks would produce the first call stack; but it should also be possible to produce the second call stack (@matthewloring was this conversation with you?).

So for this PR the first stack above is expected, but we should also address how to produce the second. IIRC the API in in place to allow watching for resolve or reject calls. At which point a stack can be produced then (somehow?) attached to the Promise handle that's responsible for the current execution scope.

// Want currentId() to return the correct value from the callbacks.
AsyncHooks::ExecScope exec_scope(env(), get_id(), get_trigger_id());
Environment::AsyncHooks::ExecScope exec_scope(env(),
get_id(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Grabbing the Local<Context> using Environment isn't free. Maybe pass it in?

Copy link
Member

Choose a reason for hiding this comment

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

Grabbing the Local<Context> using Environment isn't free

Are you sure? We use the StrongPersistentToLocal utility to get a Local out of the Persistent properties, which is somewhat icky but should compile down to a no-op.

Copy link
Contributor

Choose a reason for hiding this comment

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

@addaleax Note sure what I was smoking. My comment makes no sense. Never mind.

Local<v8::Value> external_wrap =
promise->Get(context, env->promise_async_id()).ToLocalChecked();
PromiseWrap* wrap =
static_cast<PromiseWrap*>(v8::External::Cast(*external_wrap)->Value());
Copy link
Contributor

Choose a reason for hiding this comment

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

@addaleax Don't they basically result in the same operation? e.g. from class Local:

  template <class S>
  V8_INLINE Local<S> As() const {
    return Local<S>::Cast(*this);
  }

@matthewloring To satiate my paranoia, mind adding a CHECK_NE(wrap, nullptr) just below?

@addaleax
Copy link
Member

addaleax commented May 21, 2017

@matthewloring I went ahead and pushed fixes for some of mine and Trevor’s review comments to this PR, feel free to remove anything that you disagree with.

Edit: Just so you know, I’m especially unsure about the last commit. I think it makes sense, but I’m not 100 % sure.

@trevnorris @AndreasMadsen Please take a look, and it would be great if you could pinpoint what you think should definitely be changed before this is ready to land.

CI: https://ci.nodejs.org/job/node-test-commit/10055/

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. But @trevnorris should approve before merging.

Left a few comments, mostly just nits and questions.

Local<Object> obj = tem->NewInstance(context).ToLocalChecked();
PromiseWrap* wrap = new PromiseWrap(env, obj);
promise->DefineOwnProperty(context,
env->promise_async_id(),
Copy link
Member

Choose a reason for hiding this comment

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

nit: async_id is confusion as that indicates this store the double id. But in reality, it stores the PromiseWrap object. Maybe calling it env->promise_wrap() would be better.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

v8::External::New(env->isolate(), wrap),
v8::PropertyAttribute::DontEnum).FromJust();
promise->DefineOwnProperty(context,
env->promise_async_tag(),
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 promise_async_tag() used for, preventing garbage collection? If so I think a comment should be added.

Copy link
Author

Choose a reason for hiding this comment

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

The only reference to it is held by the promise. We use its lifespan to approximate when the promise was garbage collected. I've added a comment describing this.

}

process.on('exit', onexit);
function onexit() {
Copy link
Member

Choose a reason for hiding this comment

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

We don't check the destroy hook. I'm okay with skipping it because it involves the gc and that can be a bit fiddly. But a comment should be added explaining why it isn't checked.

Copy link
Author

Choose a reason for hiding this comment

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

Yup, done!

@matthewloring matthewloring added this to the 8.0.0 milestone May 23, 2017
@matthewloring matthewloring requested a review from ofrobots May 23, 2017 17:58
@addaleax
Copy link
Member

@matthewloring The nice thing about e1c11e7 should be that the performance impact would only apply if one is using async hooks, so it doesn’t affect regular promise users.

@matthewloring
Copy link
Author

e1c11e7 makes things faster when async hooks is disabled. #13175 makes things faster when they are enabled. I think we want both in the final product.

Copy link
Member

@addaleax addaleax left a comment

Choose a reason for hiding this comment

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

Didn’t go through the test in detail but generally LGTM

info.GetReturnValue().Set(
info.Data().As<Object>()->Get(context,
env->domain_string()).ToLocalChecked());
}
Copy link
Member

Choose a reason for hiding this comment

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

I guess I missed that, but you can drop this (and the if (env->in_domain()) { block that installs it, too) now

Copy link
Author

Choose a reason for hiding this comment

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

Removed.

@addaleax
Copy link
Member

I think we want both in the final product.

Definitely, yes! I’m just eyeing a bit on the 8.0.0 release date, that’s all.

@matthewloring
Copy link
Author

If we modify the shape of promises in this PR and then revert that after the 8.0 release will that be considered breaking? I would be ok working the promise internal field changes in afterwards if we can be pretty sure it will land.

@matthewloring
Copy link
Author

In either case, I'll look into addressing the rest of Trevor's comments this afternoon.

@addaleax
Copy link
Member

If we modify the shape of promises in this PR and then revert that after the 8.0 release will that be considered breaking?

I don’t think so; I’d say it’s obvious those fields are internal, and we do consider async_hooks an experimental feature so I think we have the liberty to make that change.

I would be ok working the promise internal field changes in afterwards if we can be pretty sure it will land.

You can also rebase this PR on #13175 now, if you think that makes the most sense :) We’ll just consider this blocked until #13175 lands then.

@matthewloring
Copy link
Author

@trevnorris I think your comments should be addressed. Could you take another look.

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

@addaleax addaleax dismissed trevnorris’s stale review May 24, 2017 18:26

As far as I can tell, this has been addressed.

@addaleax
Copy link
Member

@matthewloring
Copy link
Author

New CI run is green: https://ci.nodejs.org/job/node-test-commit/10139/

@matthewloring
Copy link
Author

I'll plan to land this tomorrow if there are no objections.

@addaleax
Copy link
Member

Landed in 920278b, thanks a lot for all the work here!

@addaleax addaleax closed this May 25, 2017
addaleax pushed a commit that referenced this pull request May 25, 2017
This change provides unified tracking of asynchronous promise lifecycles
for both domains and async hooks.

PR-URL: #13000
Reviewed-By: Andreas Madsen <amwebdk@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
@Fishrock123
Copy link
Contributor

I still think if this returns the Promise as the resource that is probably the wrong behavior due to what had been discussed?

jasnell pushed a commit that referenced this pull request May 25, 2017
This change provides unified tracking of asynchronous promise lifecycles
for both domains and async hooks.

PR-URL: #13000
Reviewed-By: Andreas Madsen <amwebdk@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
jasnell pushed a commit that referenced this pull request May 28, 2017
This change provides unified tracking of asynchronous promise lifecycles
for both domains and async hooks.

PR-URL: #13000
Reviewed-By: Andreas Madsen <amwebdk@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
@jasnell jasnell mentioned this pull request May 28, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c++ Issues and PRs that require attention from people who are familiar with C++.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants