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

Optimize timer._unrefActive #268

Closed
piscisaureus opened this issue Jan 9, 2015 · 5 comments
Closed

Optimize timer._unrefActive #268

piscisaureus opened this issue Jan 9, 2015 · 5 comments
Assignees
Labels
timers Issues and PRs related to the timers subsystem / setImmediate, setInterval, setTimeout.
Milestone

Comments

@piscisaureus
Copy link
Contributor

@misterdjules pointed out that node had become significantly faster after optimizing timer._unrefActive(). To get the idea: https://github.com/joyent/node/commits/v0.10/lib/timers.js.

It turns out that timer._unrefActive() was added somewhere in 2013 to fix an issue related to unref()ing http requests; when an http request object is unref'ed, it's associated timeout timer also needs to be unref'ed or it'll keep the loop open. But it's really a performance bottleneck; _unrefActive() performs what is basically a linear scan of a linked list.

What seems more reasonable is to make timers that are stored in the linked list (through Timer.active()) always be unref'ed, and just use a separate TimerWrap object for all user timers that are scheduled through setTimeout/setInterval.

@bnoordhuis
Copy link
Member

I don't think that splitting it into two lists is going to help much, unless you want to create a timer handle per tcp/udp/pipe handle.

There is currently a lot of low hanging fruit in the timers module, it just needs someone that wants to work on it for a few days. For example, .unref() always creates a new timer handle but that's not strictly necessary; the module can just as easily maintain a global refcount and ref/unref the main timer handle.

Another probable win is the introduction of a timer wheel. In most applications, the majority of timers are short-lived and never expire because the predominant use case is to time out network I/O after a good amount of time has elapsed. Most I/O however completes in milliseconds and afterwards the timer is deleted or reset.

A reset is when the timeout is extended for (for example) another 30 seconds after a read or write. With the current implementation, resetting consists of a O(1) deletion and O(n) insertion, where n is normally at least the number of open connections, so it's easily in the low thousands. With a timer wheel, that's reduced to O(1) deletion and O(1) insertion.

As an intermediate step, switching to binary search would lower the overhead to O(log n) for insertion and deletion. It's probably just as much work to implement binary search as it is to switch to a timer wheel, however.

The one (albeit minor) downside to a timer wheel is that you have to rehash timers every now and then. However, the beauty of it is that most timers are repeatedly reset and those always stay in the highest bucket, they are never rehashed.

Yet another optimization path is to try reduce the number of calls to Date.now() / Timer.now(). Obtaining the current system time is often outrageously expensive in virtual machines because the RDTSC instruction or HPET access or whatever the time source is, is intercepted and emulated by the host. It's not uncommon to see upwards of 20% wall clock time being wasted in clock_gettime() and gettimeofday() system calls.

A harebrained idea I had is to start a thread that consists of a loop that wakes up every millisecond, calls clock_gettime() and writes the result to an atomic variable. The main thread then has cheap but accurate time; the penalty is offloaded to the other thread.

@piscisaureus piscisaureus mentioned this issue Jan 10, 2015
26 tasks
@trevnorris trevnorris added timers Issues and PRs related to the timers subsystem / setImmediate, setInterval, setTimeout. enhancement labels Jan 22, 2015
@Fishrock123
Copy link
Contributor

Trying to take a look at this, did some reading on timer wheels. [1] [2] (that being said, our implementation with handles means i'm still a bit confused. :))

@bnoordhuis

I don't think that splitting it into two lists is going to help much

Don't we already have two? unrefList and lists?

With a timer wheel, that's reduced to O(1) deletion and O(1) insertion.

Not strictly, unless the timer wheel is very large & more than one timers never fire on the same tick.

Also, I'm not really sure how this would get rid of the aforementioned unrefList?

As an intermediate step, switching to binary search would lower the overhead to O(log n) for insertion and deletion

How would this effect deletion? This does seem like an obvious win for setting timers though. But, we still have the two lists.

@Fishrock123
Copy link
Contributor

@Fishrock123
Copy link
Contributor

https://gist.github.com/Fishrock123/23950d26346dc8077a08

@bnoordhuis Even in cases that should be horrible (to my best understanding) for O(n) on timeout (Tests 3 & 4), it still performs over 2x as well as a sorted array. At minimum, I move we accept the joyent/node patches for it.

The heap impl (primarly made by Ben) is probably usable even in this state without severe optimizations, it seems generally better for the common case, and consistent even to the uncommon case (lots of timeouts).

misterdjules pushed a commit that referenced this issue Sep 2, 2015
Before this change, _unrefActive would keep the unrefList sorted when
adding a new timer.

Because _unrefActive is called extremely frequently, this linear scan
(O(n) at worse) would make _unrefActive show high in the list of
contributors when profiling CPU usage.

This commit changes _unrefActive so that it doesn't try to keep the
unrefList sorted. The insertion thus happens in constant time.

However, when a timer expires, unrefTimeout has to go through the whole
unrefList because it's not ordered anymore.

It is usually not large enough to have a significant impact on
performance because:
- Most of the time, the timers will be removed before unrefTimeout is
  called because their users (sockets mainly) cancel them when an I/O
  operation takes place.
- If they're not, it means that some I/O took a long time to happen, and
  the initiator of subsequents I/O operations that would add more timers
  has to wait for them to complete.

With this change, _unrefActive does not show as a significant
contributor in CPU profiling reports anymore.

Fixes: nodejs/node-v0.x-archive#8160

Signed-off-by: Timothy J Fontaine <tjfontaine@gmail.com>

Conflicts:
	lib/timers.js

Fixes: nodejs/node-convergence-archive#23
Ref: #268
PR-URL: #2540
Reviewed-By: bnoordhuis - Ben Noordhuis <info@bnoordhuis.nl>
misterdjules pushed a commit that referenced this issue Sep 2, 2015
Commit 934bfe2 had introduced a
regression where node would crash trying to access a null unref timer if
a given unref timer's callback would remove other unref timers set to
fire in the future.

More generally, it makes the unrefTimeout function more solid by not
mutating the unrefList while traversing it.

Fixes: nodejs/node-v0.x-archive#8897

Conflicts:
	lib/timers.js

Fixes: nodejs/node-convergence-archive#23
Ref: #268
PR-URL: #2540
Reviewed-By: bnoordhuis - Ben Noordhuis <info@bnoordhuis.nl>
Fishrock123 added a commit that referenced this issue Sep 2, 2015
This commit addresses most of the review comments in
#2540, which are kept in this
separate commit so as to better preserve the prior two patches as they
landed in 0.12.

This commit:
- Fixes a bug with unrefActive timers and disposed domains.
- Fixes a bug with unrolling an unrefActive timer from another.
- Adds a test for both above bugs.
- Improves check logic, making it stricter, simpler, or both.
- Optimizes nicer with a smaller, separate function for the try/catch.

Fixes: nodejs/node-convergence-archive#23
Ref: #268
PR-URL: #2540
Reviewed-By: bnoordhuis - Ben Noordhuis <info@bnoordhuis.nl>
@Fishrock123
Copy link
Contributor

Closing while keeping in mind that I can try to optimize it further. When I have some time.

misterdjules pushed a commit that referenced this issue Sep 3, 2015
Before this change, _unrefActive would keep the unrefList sorted when
adding a new timer.

Because _unrefActive is called extremely frequently, this linear scan
(O(n) at worse) would make _unrefActive show high in the list of
contributors when profiling CPU usage.

This commit changes _unrefActive so that it doesn't try to keep the
unrefList sorted. The insertion thus happens in constant time.

However, when a timer expires, unrefTimeout has to go through the whole
unrefList because it's not ordered anymore.

It is usually not large enough to have a significant impact on
performance because:
- Most of the time, the timers will be removed before unrefTimeout is
  called because their users (sockets mainly) cancel them when an I/O
  operation takes place.
- If they're not, it means that some I/O took a long time to happen, and
  the initiator of subsequents I/O operations that would add more timers
  has to wait for them to complete.

With this change, _unrefActive does not show as a significant
contributor in CPU profiling reports anymore.

Fixes: nodejs/node-v0.x-archive#8160

Signed-off-by: Timothy J Fontaine <tjfontaine@gmail.com>

Conflicts:
	lib/timers.js

Fixes: nodejs/node-convergence-archive#23
Ref: #268
PR-URL: #2540
Reviewed-By: bnoordhuis - Ben Noordhuis <info@bnoordhuis.nl>
misterdjules pushed a commit that referenced this issue Sep 3, 2015
Commit 934bfe2 had introduced a
regression where node would crash trying to access a null unref timer if
a given unref timer's callback would remove other unref timers set to
fire in the future.

More generally, it makes the unrefTimeout function more solid by not
mutating the unrefList while traversing it.

Fixes: nodejs/node-v0.x-archive#8897

Conflicts:
	lib/timers.js

Fixes: nodejs/node-convergence-archive#23
Ref: #268
PR-URL: #2540
Reviewed-By: bnoordhuis - Ben Noordhuis <info@bnoordhuis.nl>
Fishrock123 added a commit that referenced this issue Sep 3, 2015
This commit addresses most of the review comments in
#2540, which are kept in this
separate commit so as to better preserve the prior two patches as they
landed in 0.12.

This commit:
- Fixes a bug with unrefActive timers and disposed domains.
- Fixes a bug with unrolling an unrefActive timer from another.
- Adds a test for both above bugs.
- Improves check logic, making it stricter, simpler, or both.
- Optimizes nicer with a smaller, separate function for the try/catch.

Fixes: nodejs/node-convergence-archive#23
Ref: #268
PR-URL: #2540
Reviewed-By: bnoordhuis - Ben Noordhuis <info@bnoordhuis.nl>
Fishrock123 pushed a commit to Fishrock123/node that referenced this issue Sep 3, 2015
Before this change, _unrefActive would keep the unrefList sorted when
adding a new timer.

Because _unrefActive is called extremely frequently, this linear scan
(O(n) at worse) would make _unrefActive show high in the list of
contributors when profiling CPU usage.

This commit changes _unrefActive so that it doesn't try to keep the
unrefList sorted. The insertion thus happens in constant time.

However, when a timer expires, unrefTimeout has to go through the whole
unrefList because it's not ordered anymore.

It is usually not large enough to have a significant impact on
performance because:
- Most of the time, the timers will be removed before unrefTimeout is
  called because their users (sockets mainly) cancel them when an I/O
  operation takes place.
- If they're not, it means that some I/O took a long time to happen, and
  the initiator of subsequents I/O operations that would add more timers
  has to wait for them to complete.

With this change, _unrefActive does not show as a significant
contributor in CPU profiling reports anymore.

Fixes: nodejs/node-v0.x-archive#8160

Signed-off-by: Timothy J Fontaine <tjfontaine@gmail.com>

Conflicts:
	lib/timers.js

Fixes: nodejs/node-convergence-archive#23
Ref: nodejs#268
PR-URL: nodejs#2540
Reviewed-By: bnoordhuis - Ben Noordhuis <info@bnoordhuis.nl>
Fishrock123 pushed a commit to Fishrock123/node that referenced this issue Sep 3, 2015
Commit 934bfe2 had introduced a
regression where node would crash trying to access a null unref timer if
a given unref timer's callback would remove other unref timers set to
fire in the future.

More generally, it makes the unrefTimeout function more solid by not
mutating the unrefList while traversing it.

Fixes: nodejs/node-v0.x-archive#8897

Conflicts:
	lib/timers.js

Fixes: nodejs/node-convergence-archive#23
Ref: nodejs#268
PR-URL: nodejs#2540
Reviewed-By: bnoordhuis - Ben Noordhuis <info@bnoordhuis.nl>
Fishrock123 added a commit to Fishrock123/node that referenced this issue Sep 3, 2015
This commit addresses most of the review comments in
nodejs#2540, which are kept in this
separate commit so as to better preserve the prior two patches as they
landed in 0.12.

This commit:
- Fixes a bug with unrefActive timers and disposed domains.
- Fixes a bug with unrolling an unrefActive timer from another.
- Adds a test for both above bugs.
- Improves check logic, making it stricter, simpler, or both.
- Optimizes nicer with a smaller, separate function for the try/catch.

Fixes: nodejs/node-convergence-archive#23
Ref: nodejs#268
PR-URL: nodejs#2540
Reviewed-By: bnoordhuis - Ben Noordhuis <info@bnoordhuis.nl>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
timers Issues and PRs related to the timers subsystem / setImmediate, setInterval, setTimeout.
Projects
None yet
Development

No branches or pull requests

5 participants