Repository navigation
Optimizing 'unreferenced' timers #16105
Description
tl;dr: Timer#unref() is not very efficient, and is also confusing. This should be fixable.
Timers use pooled handles behind the hood in all normal cases. There is one exception: if a timer has been unreferenced by using Timer#unref(). (But not _unrefActive, which pools the timer but also resets it.)
#8372 Is my pervious attempt at optimizing it. As noted in that PR, that method is very odd and IIRC probably also has some behvaior weirdness in slightly-edge-cases.
A better solution is outlined by @misterdjules in that issue. I am describing it below in clearer detail so that other may better understand it. It assumes basic knowledge of our timers algorithm/implementation, most of which can be found in the extensive comments near the top of lib/timers.js.
From #8372 (comment):
I wanted to experiment with another approach to making unrefed timers use
TimerListinstances, but without the inconsistency of having some of them not useTimersList.
Each TimersList could keep a counting tracker of how many unrefed timers it contains. This would be incremented / decremented whenever Timeout#unref() / Timeout#ref() are called, and also when unref timers call their callbacks. Whenever this counter is adjusted the implementation would check if there are unrefed timers, and then either unref or ref the TimerWrap handle accordingly.
Additionally, it could also cover _unrefActive, and pool our internal unreferenced timers without needed separate pools, even for the same duration.
The logic behind this revolves around the fact that unrefed handles don't matter if there are still refed handles.
Also, this means we wouldn't need separate _handle properties on unreferenced timers, which is honestly just a confusing behavior as it stands today.
The major consideration here is whether unrefing and re-refing a libuv handle has much overhead. My recollection was that it did not have significant overhead (from @trevnorris, I think), but that would need to be investigated alongside this. (See below.) Keep in mind timers is potentially hot code for any net request and does actually need to be quite efficient.
Activity
bnoordhuis commentedon Oct 9, 2017
Better, all JS timers in node.js could be multiplexed into a single libuv
uv_timer_tinstance. That timer would then be ref'd or unref'd when necessary.uv_ref()anduv_unref()are practically free. The cost inTimeout#unref()is thev8::Persistenthandle thatnew TimerWrap()creates - it adds GC overhead.Fishrock123 commentedon Oct 9, 2017
I am curious how you'd make that more efficient than today? The pooling for regular timers means that handles should be created infrequently - are you seeing something else?
bnoordhuis commentedon Oct 9, 2017
With the scheme I propose there wouldn't need to be a pool because ref/unref would be as cheap as updating the global ref count. You don't even need to move the timer from one linked list to another, it could all be a single list.
Fishrock123 commentedon Oct 10, 2017
@bnoordhuis then we need to do order sorting though? It's what we already moved away from. 😛
bnoordhuis commentedon Oct 10, 2017
Total ordering is a net positive, all other things being equal, right?
I have a binary heap implementation in JS that could be used, or it could be done as a timer wheel. A timer wheel is probably best because most timers get cancelled long before they expire and would never make it out of the first bucket.
Apropos timer wheels: the current implementation already resembles one if you don't look closely, but it's more complex than it needs to be. I guesstimate it could be redone in 1/3rd of the current line count.
Fishrock123 commentedon Oct 11, 2017
We already get that cheaper, though. I've taken a look at a binary heap or timers wheel in the past and they really don't work better for our case in my experience.
However, this particular issue is about something more specific that should be independently solvable without changing absolutely everything.
bnoordhuis commentedon Nov 3, 2017
Sorry for hijacking but I was looking into this issue today and I noticed that deletion in random order is 4x slower than sorted or semi-sorted.
With setTimeout and clearTimeout stubs to measure the overhead:
Showing a pretty substantial 5 us to insert or delete.
apapirovski commentedon Feb 12, 2018
FWIW I have an implementation of this done that makes explicitly
.unref()or.ref()timers about 700-800% faster. I'm still not quite happy with it so it might be another week before I open PR. I've assigned to myself so no one accidentally ends up duplicating this work.Funny enough, I ended up finding this issue after I got about 80% into it and started exploring timer wheels which led me to @bnoordhuis' comment here.
I wish there a was a way to surface interesting issues like this but with 637 of them open, it's more than a little challenging.
bnoordhuis commentedon Feb 12, 2018
I know and feel your pain.
timers: use only a single TimerWrap instance
lib: add internal PriorityQueue class