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

timers: optimize same-tick unref #8372

Closed
wants to merge 2 commits into from

Conversation

Fishrock123
Copy link
Contributor

@Fishrock123 Fishrock123 commented Sep 1, 2016

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

timers

Description of change

The logic behind this revolves around the following:

  1. A timer will never fire before nextTick.
    (Thus it is ok to schedule it during nextTick.)
  2. Timer properties are unimportant until scheduled.

See discussion in #6699 for more
background.

CI: https://ci.nodejs.org/job/node-test-pull-request/3919/

@nodejs-github-bot nodejs-github-bot added the timers Issues and PRs related to the timers subsystem / setImmediate, setInterval, setTimeout. label Sep 1, 2016
@Fishrock123 Fishrock123 added the wip Issues and PRs that are still a work in progress. label Sep 1, 2016
@Fishrock123
Copy link
Contributor Author

@misterdjules how do you feel about this?

It should be possible to shim _handle so as to make sure this does not break anything. (but this does not yet do that.)

@Fishrock123 Fishrock123 changed the title Timers optimize unref timers: optimize same-tick unref Sep 1, 2016
@mscdex mscdex added the performance Issues and PRs related to the performance of Node.js. label Sep 1, 2016
@misterdjules
Copy link

misterdjules commented Sep 2, 2016

Thank you for the heads up @Fishrock123, I appreciate it!

I don't understand what the goal of this PR is. It's likely that I'm missing something obvious, so please consider the questions below as genuine questions to help me understand what this PR is about, and not as rhetorical questions aimed at dismissing it.

In the title, it mentions that it optimizes something, but I don't understand what use case or code path is optimized, and in what way. So my first question is: what problem does this PR solve? If this is about making some code path(s) run faster, are there some benchmarks somewhere that we can take a look at?

It also refers to #6699, but my understanding of #6699 is that it is about being able to differentiate between internal/external timers correctly. How is this PR related to that?

Lastly, the second comment of this PR mentions " shim[ing] _handle so as to make sure this does not break anything.". How does this PR breaks anything related to using the _handle property?

Thank you!

@Fishrock123
Copy link
Contributor Author

In the title, it mentions that it optimizes something, but I don't understand what use case or code path is optimized, and it what way. So my first question is: what problem does this PR solve? If this is about making some code path(s) run faster, are there some benchmarks somewhere that we can take a look at?

This makes same-tick unref() use the internal unref handle pooling.

e.g.

// optimized
setTimeout(() => {}, 100).unref()

// not optimized
const time r= setTimeout(() => {}, 100)
setImmediate(() => {
  timer.unref()
})

How is this PR related to that?

It is not. It is related to the discussions therein.

lib/timers.js Outdated
function insertNT() {
insertNTScheduled = false;
let timer;
while (timer = queuedTimers.shift()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

If this is about performance, wouldn't a linked list make more sense here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Doh, yes.

The logic behind this revolves around the following:
1. A timer will never fire before nextTick.
  - Thus it is ok to schedule it during nextTick.
2. Timer properties are unimportant until scheduled.

See discussion in nodejs#6699 for more
background.
@Fishrock123
Copy link
Contributor Author

Updated with @ronkorving's suggestion.

How does this PR breaks anything related to using the _handle property?

This currently does not ensure there is always a _handle property on unref() -- if you hit the optimization it won't be there.
However, exposing the pooled handle is "dangerous" since someone may accidentally close more than they expect. As such, we should "shim" _handle for those. (WIP)

this.owner._onTimeout();
if (!this.owner._repeat)
this.owner.close();
}

function createOwnHandle() {
var now = TimerWrap.now();
Copy link
Member

Choose a reason for hiding this comment

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

extremely minor nit: this could/should be const

@misterdjules
Copy link

Just to make sure I understand this correctly: is one goal of this PR to make unrefed external timers use TimersList instances to avoid creating a separate TimerWrap instance for each unrefed external timer that has the same delay?

If so, what prevents us from doing that even in the following example you mentioned previously:

const time r= setTimeout(() => {}, 100)
setImmediate(() => {
  timer.unref()
})

?

It seems to me that this PR tries to achieve two things that are orthogonal to each other:

  1. Make external unrefed timers not create a TimerWrap instance for each of them.
  2. Optimize setTimeout by scheduling its heavy lifting to when nextTick callbacks are processed.

Is that correct? If these two points are orthogonal, I would suggest implementing them in two separate PRs.

I would also like to see numbers that illustrate the impact of point 2) on performance.

@Fishrock123
Copy link
Contributor Author

Fishrock123 commented Sep 2, 2016

Just to make sure I understand this correctly: is one goal of this PR to make unrefed external timers use TimersList instances to avoid creating a separate TimerWrap instance for each unrefed external timer that has the same delay?

Effectively.

If so, what prevents us from doing that even in the following example you mentioned previously:

@misterdjules if you know how to do that arbitrarily delayed linked-list insert in less-than-linear (O(n)) time, please let me know. :)

Note: This is the problem I have been trying to work around the entire time.

Edit: even with a sorting algo, you cannot insert or view arbitrary places in the list without traversing it.

I would also like to see numbers that illustrate the impact of point 2) on performance.

I will get numbers if it comes down to the line but the only thing significant added is the nextTick. Grabbing a timer from that new list is O(1) per timer. Negligible.

@misterdjules
Copy link

If so, what prevents us from doing that even in the following example you mentioned previously:

@misterdjules if you know how to do that arbitrarily delayed linked-list insert in less-than-linear (O(n)) time, please let me know. :)

Note: This is the problem I have been trying to work around the entire time.

Edit: even with a sorting algo, you cannot insert or view arbitrary places in the list without traversing it.

Right, I wasn't aware of the trade-offs you had in mind, but that clarifies things, thanks!

Would unrefing external timers be called often enough in most use cases that the insertion time would be a problem? It was in the case of internal (and by definition unrefed) timers because these timers were restarted and thus re-inserted very frequently (like on every I/O event on a given net.Socket), but it seems that it might not be the case of external unrefed timers.

Coming up with sample code and benchmarks that represents the typical use cases we have in mind could help here.

I'm asking these questions because my first impression is that the inconsistency between external unrefed timers that share a TimersList's TimerWrap instance and those who have their own TimerWrap instance makes things more complex for node's core maintainers and potentially for users too, depending on what the _handle shim you mentioned looks like.

I would also like to see numbers that illustrate the impact of point 2) on performance.

I will get numbers if it comes down to the line but the only thing significant added is the nextTick. Grabbing a timer from that new list is O(1) per timer. Negligible.

I realize now that the use of a nextTick callback to insert timers was not necessarily meant to be an optimization in itself, but was just a way to know whether an external timer should be unrefed at insertion time. Is that correct? If so then what I said about separating these two changes wouldn't make sense, and we wouldn't need separate benchmarks for that.

@Fishrock123
Copy link
Contributor Author

Fishrock123 commented Sep 3, 2016

but was just a way to know whether an external timer should be unrefed at insertion time. Is that correct?

Correct. It's like the Promises & catch() problem. There is no way to know if it follows it directly, or is called at some point in the future, if at all.

Would unrefing external timers be called often enough in most use cases that the insertion time would be a problem? It was in the case of internal (and by definition unrefed) timers because these timers were restarted and thus re-inserted very frequently (like on every I/O event on a given net.Socket), but it seems that it might not be the case of external unrefed timers.

I really have no idea, it would also be very hard to identify in the wild, buried in async layers. It could potentially tank perf pretty hard though in any application using lots of TCP connections.

Edit: hard may still be minor, but it could become significant in any perf profiles.

I originally was thinking of having something like setTimeoutUnref()... but that's adding more APIs and isn't browser-consistent anymore.

depending on what the _handle shim you mentioned looks like.

It would probably look exactly like a TimerWrap, except it wouldn't behave 100% like one under the hood, i.e. closing it would just close that timer and not the entire pool.

@misterdjules
Copy link

Would unrefing external timers be called often enough in most use cases that the insertion time would be a problem?

I really have no idea, it would also be very hard to identify in the wild, buried in async layers. It could potentially tank perf pretty hard though in any application using lots of TCP connections.

That wasn't clear from my original question: what I wanted to ask is whether unrefing a timer asynchronously is a common use case. If not, then it might be OK to have such unref calls be slower. I'm wondering if there are use cases when calling unref asynchronously allows users to do something that calling it synchronously doesn't allow. Do you have examples in mind?

I wanted to experiment with another approach to making unrefed timers use TimerList instances, but without the inconsistency of having some of them not use TimersList. I tried having Timeout instances store a reference to their TimersList instance, and have Timeout.prototype.{ref,unref} methods increment/decrement a refed counter to determine when their associated TimersList instance's underlying timer handler can be unrefed.

The result is a WIP commit that still needs to deal with the issue you mentioned with the _handle property of each Timeout instance. That is, with that change, someTimer._handle.close() closes the underlying TimerWrap instance, regardless of the state of other Timeout instance that belong to the same timer list. It might be possible to solve that problem, but I wanted to share that experiment with you before moving forward with this in case you'd think it might be an interesting approach.

@Fishrock123 Fishrock123 closed this Sep 6, 2016
@Fishrock123 Fishrock123 reopened this Sep 6, 2016
@Fishrock123
Copy link
Contributor Author

Fishrock123 commented Sep 6, 2016

what I wanted to ask is whether unrefing a timer asynchronously is a common use case.

The indeterminism makes me hope not. cc @brycebaril who is the only person I know to use unref() on timers.

I'm wondering if there are use cases when calling unref asynchronously allows users to do something that calling it synchronously doesn't allow. Do you have examples in mind?

Uh, I guess you can unref some time from a downstream library you may be using? I can't think of anything else really.

The result is a WIP commit that still needs to deal with the issue you mentioned with the _handle property of each Timeout instance.

Huh. That is an interesting but somewhat more complex approach.

Am I correct in saying that the logic behind this revolves around the fact that unrefed handles don't matter if there are still refed handles?

I suppose the big question would be if refing and unrefing a handle potentially a large number of times poses any cost on perf and/or anything else. cc @trevnorris?

@brycebaril
Copy link
Contributor

what I wanted to ask is whether unrefing a timer asynchronously is a common use case.
The indeterminism makes me hope not. cc @brycebaril who is the only person I know to use unref() on timers.

Pretty much every time I've used it, the unref call is synchronous, though I can imagine rare cases where you wouldn't immediately know if you needed to call unref or not.

In either case typically the total number of live timers at any given time shouldn't be that large for most use-cases. Any timer-centric use-case beyond that is probably a lot less likely to have unref'd timers.

@misterdjules
Copy link

@Fishrock123

Am I correct in saying that the logic behind this revolves around the fact that unrefed handles don't matter if there are still refed handles?

That is correct.

@jasnell
Copy link
Member

jasnell commented Mar 1, 2017

@Fishrock123 ... still interested in this?

@BridgeAR
Copy link
Member

Ping @Fishrock123

@jasnell
Copy link
Member

jasnell commented Aug 29, 2017

@BridgeAR ... given the lack of any activity, I think it's safe just to close this. @Fishrock123 can reopen if it's something he's still interested in pursuing

@jasnell jasnell closed this Aug 29, 2017
@BridgeAR
Copy link
Member

I suppose the big question would be if refing and unrefing a handle potentially a large number of times poses any cost on perf and/or anything else

I am relatively certain that I actually ran into this issue once and that I removed the refing / unrefing again because of the immense performance penalty. Therefore I think it would still be nice to get this in.

That is why I still had hope 😄

@jasnell
Copy link
Member

jasnell commented Aug 29, 2017

Yeah, it's a shame these older PRs weren't followed up on

@Fishrock123
Copy link
Contributor Author

the discussion here is valuable and should be moved into an issue

@Fishrock123
Copy link
Contributor Author

Moving to the following issue I've created, which should really have been done instead of just hands-off closing this. How will anyone ever know that they could potentially pick this up otherwise?

#16105

@Fishrock123 Fishrock123 closed this Oct 9, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
performance Issues and PRs related to the performance of Node.js. stalled Issues and PRs that are stalled. timers Issues and PRs related to the timers subsystem / setImmediate, setInterval, setTimeout. wip Issues and PRs that are still a work in progress.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants