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

Add context argument to emitter #15763

Closed
hueniverse opened this issue Oct 3, 2017 · 26 comments
Closed

Add context argument to emitter #15763

hueniverse opened this issue Oct 3, 2017 · 26 comments
Labels
events Issues and PRs related to the events subsystem / EventEmitter. feature request Issues that request new features to be added to Node.js. stalled Issues and PRs that are stalled.

Comments

@hueniverse
Copy link

As part of the hapi v17 work, I removed every closure from the hot path. This provided significant performance improvements. All these closures came from handlers passed to node EventEmitters (specifically from req and res object). In order to do that, I had to hang my own property on the emitter object. For example:

const handler = function (err) {
    this._context.log(err);
};

// Inside some prototype methods:

req._context= this;
req.on('error', handler);

This works and can be improved by using symbols, but it's still messy. It also means adding multiple context properties per handler because they might need different context data. Instead, I would like to be able to do something as simple as:

const handler = function (err, context) {
    context.log(err);
};

// Inside some prototype methods:

req.on('error', handler, this);

The idea is to pass a third optional argument to on() and then append that argument to the end of the emit arguments list. We already store listeners as objects with properties. When this feature is not used, it means two additional if statements: once to check if a third argument was provided and another to check if one is present and needs to be appended. If no third argument is provided, these two extra if statements should not have any noticeable impact on performance.

However, since 99% of the time, emitter handlers require the creation of a callback closure, this should make applications using it much faster overall. Potentially, this can be used internally by node as well.

There is already a pattern for this in setTimeout(). Most new JS features with callbacks now accept some kind of extra binding argument. Because we already bind the handler to the emitter, we cannot use this pattern.

I'm happy to do the work if the idea is acceptable.

@mscdex
Copy link
Contributor

mscdex commented Oct 3, 2017

Why not use handler.bind(...)? It is fast in recent node versions and we even use it in core in hot paths (including http).

@mscdex mscdex added events Issues and PRs related to the events subsystem / EventEmitter. feature request Issues that request new features to be added to Node.js. labels Oct 3, 2017
@hueniverse
Copy link
Author

I don't want to bind the handler. I want to leave the handler's binding as-is (the emitter). I just want to pass some random context the same way I can pass it to most other functional interfaces and timers. But more importantly, why force everyone to bind handlers when we can instead just pass an argument? This is the most common use case of emitters...

@jasnell
Copy link
Member

jasnell commented Oct 3, 2017

I'm generally +1 on the need for this, as a technical detail adding a new argument does have a non-zero risk of breaking existing code. There is another approach I've been considering that also covers another discussion that's been ongoing... that is, adding a new API to EventEmitter that allows a more generic handler to be registered for all events... e.g.

emitter.addHandler((event, context, ...args) => { /* ... */ }, context);

Here, the handler is invoked for every emitted event, with the event name passed as the first argument. I've augmented what I was thinking previously with the additional context argument.

Because this is a new API, it wouldn't have a risk of breaking existing stuff.

@hueniverse
Copy link
Author

@jasnell I like it a lot, even better than my idea!

@hueniverse
Copy link
Author

On second thought, I need to be able to choose the events because I’m dealing with streams and I don’t want to get called on every chuck of data.

@jasnell
Copy link
Member

jasnell commented Oct 4, 2017

Ok. I think the same principle applies with regards to adding a new api. But definitely can work with that constraint

@hueniverse
Copy link
Author

And your argument order is the correct one where context comes before the event arguments, not after.

@jasnell
Copy link
Member

jasnell commented Oct 4, 2017

Ok, so there are a couple of requirements from this and the other discussion ... that I think we can easily capture in a single new API.

(we can bikeshed the name of the new function later)

emitter.addListenerWithContext( [ 'event1', 'event2' ], context, (event, context, ...args) => { /* ... */ });

The context argument can be made optional here. If it is supplied, then it is passed to the handler.

@mcollina @silverwind ... what do you think?

@medikoo
Copy link

medikoo commented Oct 4, 2017

Wouldn't that make emitter unnecessary complex? Currently it's a great simple API, and use cases where we e.g. needed to handle context etc. were always without much hassle addressed in user-land.

@apapirovski
Copy link
Member

Node core itself could strongly benefit from having something like proposed above (addListenerWithContext) so 👍 here.

@mcollina
Copy link
Member

mcollina commented Oct 4, 2017

Can you add benchmarks between a) using bind vs b) changing the request? I think it would be interesting data for everyone.

I'm 👍 with this change. I would prefer the 3rd parameter to on(), but I fear that it precludes the road to land it in a semver-minor. The real blocker on this work are regressions: we could not land it if it's slower than the current code.

As you said, the current best practice involves modifying the request, however this often leads to every request having different "shapes" on the V8 eyes, and it prevents a great deal of optimisations from their side. If you are mostly interested in just attaching a context to req and res, you might want to have a look at #15752 and #15560. I think they might solve your issue in a different way (@hekike is working on Restify at the moment), and they have almost zero side-effects and potential for regressions.

@silverwind
Copy link
Contributor

Instead of adding a third boolean parameter for .on, I think we're better off introducing a options object for .on, analogous to addEventListener in browsers. context could be easily realized as a option there, not so sure about a possible multiple option as it would change the function signature, so a separate method might still be cleaner there.

@not-an-aardvark
Copy link
Contributor

Aside from performance, is there an advantage to adding a context argument rather than just using a closure in userland?

It seems to me that this isn't a very common use case (in most code paths, the slight performance penalty of using a closure is worthwhile for the benefit of more idiomatic code). I agree with @medikoo's comment in #15763 (comment) that this seems a bit like feature creep.

If we do add this to the EventEmitter API, I think we should add it as a new method rather than a third argument to on. There are a few reasons I think a new method would be better from an API design perspective:

  • Adding a new method avoids confusing new users who are just trying to understand how on works, and who usually don't need the performance benefits of context.
  • Adding context as a third argument to on would permanently block Node from adding anything else to on in the third-argument position. If we found a use-case later for passing another customization value to on, we would be forced to put it in the fourth-argument position. If a user only wanted to use that new customization value without using context, they would be forced to put null (e.g.) as the third argument. This could lead to confusing APIs in the future. (Using an object with a context property as the third argument would avoid this problem.)
  • Subjectively, I think adding a third argument would hurt readability for users who are unfamiliar with what the third argument does. (It would look like another value that gets passed to on with a seemingly arbitrary and non-obvious functionality.) Using another method or adding an object with a context property would avoid this problem.

@mscdex
Copy link
Contributor

mscdex commented Oct 4, 2017

Yeah I agree that if we should add this at all in core it should be a different method.

I still don't understand why .bind() can't be used. You can pass arguments with .bind() as well:

function handler(context, arg) {
  // context === req
  // arg === 'bar'
}

// ...

req.on('foo', handler.bind(req, contextArg));
req.emit('foo', 'bar');

@hueniverse
Copy link
Author

https://jsperf.com/emitter-bind-vs-context

25% better is meaningful.

@mscdex
Copy link
Contributor

mscdex commented Oct 5, 2017

@hueniverse FWIW I only get a ~8-9% difference with current node master when comparing actual built-in EventEmitter + bind() vs. a copy of EventEmitter with a simple context implementation + passing a context object.

It seems V8 is getting better with calling bound functions.

@hueniverse
Copy link
Author

My best argument is that every new JS API is providing this functionality one way or another. If we are not making any changes/new APIs, fine. But if we are still evolving the node APIs, this seems like an easy win. I also think node has been historically more aggressive at looking for 8-9% gains...

@not-an-aardvark
Copy link
Contributor

My best argument is that every new JS API is providing this functionality one way or another.

Could you provide some examples? I personally haven't noticed a pattern like this.

@hueniverse
Copy link
Author

@not-an-aardvark Map.prototype.forEach() and Set allow you to pass a bind option - both are newish additions.

@kanongil
Copy link
Contributor

kanongil commented Oct 5, 2017

I would just like to note that the context argument has already been done in userland, eg: https://www.npmjs.com/package/eventemitter3#contextual-emits.

@bnoordhuis
Copy link
Member

Somewhat related: #13338 - which was voted down for being unidiomatic.

My .02: if the performance gap is currently < 10%, a better strategy is to try and close that gap than add an ad hoc new API.

Map.prototype.forEach() and Set allow you to pass a bind option - both are newish additions.

But they inherit that from Array.prototype.forEach(), which is not new. And it's the thisArg, it's not an extra context argument to the callback.

If a thisArg is what is needed, then .bind() should already be quite competitive. It only starts to lose out when you prepend arguments because then V8 has to resize and copy the arguments array with each call.

@Fishrock123
Copy link
Contributor

With bind performance these days I don't really see the issue with just binding? I'm sure there are plenty other APIs that require bind for similar things.

@bnoordhuis
Copy link
Member

Relevant: v8/v8@594803c [turbofan] Inline Function#bind in more cases.

@Ulrikop
Copy link

Ulrikop commented Oct 27, 2017

I like the .bind way but In my opinion, the .bind has a drawback: You can't remove the listener without saving the binded function. eventEmitter.on('myEvent', callback.bind(this)) can't be removed from eventEmitter.removeListener('myEvent', callback.bind(this)).

Do you have a nice solution for removing an binded callback without saving it?

@bnoordhuis
Copy link
Member

@Ulrikop No, that's just something you have to learn to live with.

You'd have the same issue with a context object. You should be able to add the same function with different contexts but then you need the context object again for disambiguation when you remove one.

@jasnell jasnell added the stalled Issues and PRs that are stalled. label Aug 12, 2018
@jasnell
Copy link
Member

jasnell commented Aug 12, 2018

Closing due to lack of further progress.

@jasnell jasnell closed this as completed Aug 12, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
events Issues and PRs related to the events subsystem / EventEmitter. feature request Issues that request new features to be added to Node.js. stalled Issues and PRs that are stalled.
Projects
None yet
Development

No branches or pull requests