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

v8: patch for thenables in PromiseHook API #33560

Closed
wants to merge 1 commit into from

Conversation

Qard
Copy link
Member

@Qard Qard commented May 25, 2020

Thenables produce an additional promise which is not currently emitting before/after hooks from the PromiseHook API in V8. This change makes that additional promise also emit the proper events.

I spent some time digging into V8 trying to figure out a fix for the thenables issue which I previously attempted a higher-level fix for in #33189 and got some resistance. With this change, async_hooks and therefore AsyncLocalStorage will now be able to correctly propagate context into the then(...) method call of a thenable.

I'm submitting this as a PR here to seek help in getting this change into V8 and for us to have the option to land it as a patch, if we want. I haven't written tests for it, though I've tested it manually, as I'm not yet sure how we want to approach landing this.

cc @nodejs/v8 @nodejs/diagnostics @nodejs/async_hooks

Thenables produce an additional promise which is not currently emitting
before/after hooks from the PromiseHook API in V8. This change makes
that additional promise also emit the proper events.
@Qard Qard added discuss Issues opened for discussions and feedbacks. async_hooks Issues and PRs related to the async hooks subsystem. labels May 25, 2020
@nodejs-github-bot nodejs-github-bot added the v8 engine Issues and PRs related to the V8 dependency. label May 25, 2020
@devsnek
Copy link
Member

devsnek commented May 25, 2020

I can submit this upstream in V8 if you would like.

@benjamingr
Copy link
Member

Thank you for doing this, this change looks a lot better to me than #33189 in terms of the performance impact

@Qard
Copy link
Member Author

Qard commented May 25, 2020

@devsnek That'd be much appreciated! I'll get to work on figuring out a V8 test for it too. :)

@@ -198,11 +198,20 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask(
const TNode<Object> thenable = LoadObjectField(
microtask, PromiseResolveThenableJobTask::kThenableOffset);

// Run the promise before/debug hook if enabled.
RunPromiseHook(Runtime::kPromiseHookBefore, microtask_context,
Copy link
Member

Choose a reason for hiding this comment

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

I'm looking through the history of promise hooks (v8:4643 and such) trying to understand why this would've been left out and I haven't found anything, but it does seem suspicious...

Copy link
Member

Choose a reason for hiding this comment

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

It looks like it was just missed. I think "thenables" just weren't considered. I don't see anything in v8:4643 suggesting otherwise.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed. They have it for the other branches in there, but not thenables. Makes me think there could be a specific reason for that. As far as I can tell though it's just been overlooked. In testing it locally though, it fixes the problem perfectly for our needs.

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 there was a discussion at diagnostics summit in munich if v8 should call promise hooks for non user visible promises or not. The agreement was it is not needed as it improves performance and assumption was that no hooks are needed for "invisible" promises. Promisehooks are quite expensive for v8 - in special since they did more optimizations of promises/await that time.
To me this looks like some corner case regarding visibility of the promise created here. The Promise itself is not visible in the sense that user can access it as it is only used within the await statemachine. On the other hand the side effects are visible.

@devsnek
Copy link
Member

devsnek commented May 26, 2020

https://crrev.com/c/2215624

@Qard
Copy link
Member Author

Qard commented May 26, 2020

Thanks, @devsnek!

@mcollina
Copy link
Member

This seems the path to follow, good work!

Have you run the async_hooks benchmarks to see if this impacts at all?

@Flarna
Copy link
Member

Flarna commented May 26, 2020

Good finding!

I expect the impact havily depends on the sample. Currently we have no sample using thenables or non native promises.

@Qard
Copy link
Member Author

Qard commented May 26, 2020

I haven't run it through the benchmarks, but just structurally, it's a branch within another branch so it should have zero cost in any non-thenable case, and only a single load and branch in the thenable case, so basically zero. 🤷

@AndreasMadsen
Copy link
Member

@quad Could you explain how this integrates with the async_hooks init hook? I see it emits before and after, but what about init?

@quad
Copy link

quad commented May 30, 2020 via email

@Qard
Copy link
Member Author

Qard commented May 31, 2020

So what we have now with a thenable is a wrapper promise with all lifecycle events and the thenable itself which only lacks before/after events. It was actually already producing init and promiseResolve just fine, it just lacked the microtask queue codegen stuff to emit before/after events for that specific type of promise job.

If you run this on master:

'use strict';

const { AsyncLocalStorage, createHook } = require('async_hooks');

function hookFn(name) {
  return (asyncId, type, triggerId) => {
    let message = `${name} ${asyncId}`;
    if (name === 'init') {
      message += ` for ${type} from ${triggerId}`;
    }
    process._rawDebug(message);
  };
}

createHook({
  init: hookFn('init'),
  before: hookFn('before'),
  after: hookFn('after'),
  promiseResolve: hookFn('promiseResolve')
}).enable();

const store = new AsyncLocalStorage();

function thenable() {
  return {
    then(cb) {
      console.log('store in then is', store.getStore());
      process._rawDebug('before then');
      setImmediate(cb);
      process._rawDebug('after then');
    }
  };
}

store.run({}, async () => {
  process._rawDebug('before thenable()');
  await thenable();
  process._rawDebug('after thenable()');
  console.log('store after run is', store.getStore());
});

You'll get this:

init 2 for AsyncLocalStorage from 1
before 2
init 3 for PROMISE from 2
before thenable()
init 4 for PROMISE from 3
init 5 for PROMISE from 4
promiseResolve 4
after 2
init 6 for TTYWRAP from 0
init 7 for SIGNALWRAP from 0
store in then is undefined
init 8 for TickObject from 0
before then
init 9 for Immediate from 0
after then
before 8
after 8
before 9
promiseResolve 4
after 9
before 5
after thenable()
store after run is {}
init 10 for TickObject from 5
promiseResolve 3
promiseResolve 5
after 5
before 10
after 10

Note the lack of before/after events for the promise with async id 4. Now if you run this again after the fix, you get this:

init 2 for AsyncLocalStorage from 1
before 2
init 3 for PROMISE from 2
before thenable()
init 4 for PROMISE from 3
init 5 for PROMISE from 4
promiseResolve 4
after 2
before 4
init 6 for TTYWRAP from 4
init 7 for SIGNALWRAP from 4
store in then is {}
init 8 for TickObject from 4
before then
init 9 for Immediate from 4
after then
after 4
before 8
after 8
before 9
promiseResolve 4
after 9
before 5
after thenable()
store after run is {}
init 10 for TickObject from 5
promiseResolve 3
promiseResolve 5
after 5
before 10
after 10

Here you can see the promise with async id 4 does run the before and after hooks, and the store check within the then(...) method succeeds this time.

@AndreasMadsen
Copy link
Member

Thanks, this is great 🙂

@BridgeAR BridgeAR force-pushed the master branch 2 times, most recently from 8ae28ff to 2935f72 Compare May 31, 2020 12:18
@devsnek
Copy link
Member

devsnek commented May 31, 2020

@Qard can you run some benchmarks with this enabled and disabled?

@Qard
Copy link
Member Author

Qard commented Jun 1, 2020

@devsnek Yep. I'll put something together on Monday. 👍

@Qard
Copy link
Member Author

Qard commented Jun 2, 2020

This is a run of benchmark/async_hooks/promises.js with the await in the run() method replaced with a thenable:

async function run(n) {
  for (let i = 0; i < n; i++) {
    await { then(cb) { cb(); } };
  }
}
[00:06:32|% 100| 1/1 files | 60/60 runs | 3/3 configs]: Done
                                                                    confidence improvement accuracy (*)    (**)   (***)
 async_hooks/promises.js asyncHooks='disabled' n=1000000                          -3.16 %       ±9.94% ±13.24% ±17.24%
 async_hooks/promises.js asyncHooks='enabled' n=1000000                            0.18 %       ±6.39%  ±8.50% ±11.08%
 async_hooks/promises.js asyncHooks='enabledWithDestroy' n=1000000                -1.91 %       ±7.44%  ±9.90% ±12.89%

Be aware that when doing many comparisons the risk of a false-positive
result increases. In this case there are 3 comparisons, you can thus
expect the following amount of false-positive results:
  0.15 false positives, when considering a   5% risk acceptance (*, **, ***),
  0.03 false positives, when considering a   1% risk acceptance (**, ***),
  0.00 false positives, when considering a 0.1% risk acceptance (***)

@Qard
Copy link
Member Author

Qard commented Jun 5, 2020

The fix has landed in V8. How do we proceed from here? Float a patch until we update to the version of V8 with the fix? Can we get that backported too?

@devsnek
Copy link
Member

devsnek commented Jun 5, 2020

@Qard cherry-picking it into our v8 should be fine.

@targos
Copy link
Member

targos commented Jun 6, 2020

@Qard would you like to change this PR into a cherry-pick from upstream?
The command to run with node-core-utils is git node v8 backport eec10a2fd8fa4d0e7f5e32dedbd239d938a5dd87

@Qard
Copy link
Member Author

Qard commented Jun 6, 2020

Yep, sounds good to me. I’ll see about doing that on Monday. 🙂

@Qard
Copy link
Member Author

Qard commented Jun 6, 2020

Just tried it locally and it seems to be stuck spinning at "apply patch" forever. I left it and came back an hour later to find it still spinning. 🤔

@Qard
Copy link
Member Author

Qard commented Jun 6, 2020

Ah, it appears to be due to nodejs/node-core-utils#421 which looks like it's been fixed but the fix has not yet been released. 🤔

@Qard Qard closed this Jun 6, 2020
@Flarna
Copy link
Member

Flarna commented Jun 6, 2020

PR to cherry-pick the v8 fix: #33778

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. discuss Issues opened for discussions and feedbacks. v8 engine Issues and PRs related to the V8 dependency.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants